@byline/host-tanstack-start 4.19.0 → 5.1.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 (322) hide show
  1. package/dist/admin-shell/chrome/admin-app-bar.d.ts +7 -0
  2. package/dist/admin-shell/chrome/admin-app-bar.js +10 -2
  3. package/dist/admin-shell/chrome/session-change-boundary.d.ts +14 -0
  4. package/dist/admin-shell/chrome/session-change-boundary.js +87 -0
  5. package/dist/admin-shell/chrome/session-change-boundary.test.d.ts +8 -0
  6. package/dist/admin-shell/chrome/sign-in-page.d.ts +2 -1
  7. package/dist/admin-shell/chrome/sign-in-page.js +60 -3
  8. package/dist/admin-shell/collections/edit.d.ts +1 -1
  9. package/dist/admin-shell/collections/edit.js +151 -31
  10. package/dist/admin-shell/collections/history.js +4 -2
  11. package/dist/admin-shell/collections/list.js +30 -6
  12. package/dist/admin-shell/collections/restore-version-modal.d.ts +3 -1
  13. package/dist/admin-shell/collections/restore-version-modal.js +9 -4
  14. package/dist/admin-shell/collections/tree-list.js +16 -6
  15. package/dist/admin-shell/collections/version-history.d.ts +2 -1
  16. package/dist/admin-shell/collections/version-history.js +11 -2
  17. package/dist/admin-shell/document-mutation-notice.d.ts +6 -0
  18. package/dist/admin-shell/document-mutation-notice.js +53 -0
  19. package/dist/admin-shell/document-mutation-state.d.ts +22 -0
  20. package/dist/admin-shell/document-mutation-state.js +71 -0
  21. package/dist/admin-shell/document-mutation-state.test.d.ts +1 -0
  22. package/dist/admin-shell/scheduled-publications/list.js +15 -5
  23. package/dist/admin-shell/singletons/history.js +4 -2
  24. package/dist/admin-shell/singletons/view.d.ts +1 -1
  25. package/dist/admin-shell/singletons/view.js +98 -19
  26. package/dist/integrations/byline-admin-services.js +7 -1
  27. package/dist/integrations/byline-field-services.js +2 -9
  28. package/dist/integrations/client-ip.d.ts +19 -0
  29. package/dist/integrations/client-ip.js +34 -0
  30. package/dist/integrations/session-coordination.d.ts +24 -0
  31. package/dist/integrations/session-coordination.js +85 -0
  32. package/dist/integrations/session-middleware.d.ts +11 -0
  33. package/dist/integrations/session-middleware.js +94 -0
  34. package/dist/integrations/session-request-kind.d.ts +9 -0
  35. package/dist/integrations/session-request-kind.js +10 -0
  36. package/dist/integrations/session-request-middleware.d.ts +9 -0
  37. package/dist/integrations/session-request-middleware.js +7 -0
  38. package/dist/integrations/sign-in-body.d.ts +12 -0
  39. package/dist/integrations/sign-in-body.js +52 -0
  40. package/dist/integrations/sign-in-middleware.d.ts +9 -0
  41. package/dist/integrations/sign-in-middleware.js +10 -0
  42. package/dist/routes/create-admin-layout-route.js +46 -31
  43. package/dist/routes/create-collection-edit-route.js +1 -1
  44. package/dist/routes/create-collection-list-route.d.ts +0 -7
  45. package/dist/routes/create-collection-list-route.js +7 -9
  46. package/dist/routes/create-sign-in-route.js +4 -2
  47. package/dist/routes/create-singleton-route.d.ts +2 -0
  48. package/dist/routes/create-singleton-route.js +4 -3
  49. package/dist/server-fns/admin-account/change-password.d.ts +3 -1
  50. package/dist/server-fns/admin-account/change-password.js +9 -3
  51. package/dist/server-fns/admin-account/get.d.ts +3 -1
  52. package/dist/server-fns/admin-account/get.js +4 -1
  53. package/dist/server-fns/admin-account/index.d.ts +5 -5
  54. package/dist/server-fns/admin-account/update.d.ts +3 -1
  55. package/dist/server-fns/admin-account/update.js +4 -1
  56. package/dist/server-fns/admin-activity/get.d.ts +3 -1
  57. package/dist/server-fns/admin-activity/get.js +4 -1
  58. package/dist/server-fns/admin-permissions/get-role-abilities.d.ts +3 -1
  59. package/dist/server-fns/admin-permissions/get-role-abilities.js +4 -1
  60. package/dist/server-fns/admin-permissions/index.d.ts +4 -4
  61. package/dist/server-fns/admin-permissions/list-registered.d.ts +3 -1
  62. package/dist/server-fns/admin-permissions/list-registered.js +4 -1
  63. package/dist/server-fns/admin-permissions/set-role-abilities.d.ts +3 -1
  64. package/dist/server-fns/admin-permissions/set-role-abilities.js +4 -1
  65. package/dist/server-fns/admin-permissions/who-has.d.ts +3 -1
  66. package/dist/server-fns/admin-permissions/who-has.js +4 -1
  67. package/dist/server-fns/admin-roles/create.d.ts +3 -1
  68. package/dist/server-fns/admin-roles/create.js +4 -1
  69. package/dist/server-fns/admin-roles/delete.d.ts +3 -1
  70. package/dist/server-fns/admin-roles/delete.js +4 -1
  71. package/dist/server-fns/admin-roles/get.d.ts +3 -1
  72. package/dist/server-fns/admin-roles/get.js +4 -1
  73. package/dist/server-fns/admin-roles/index.d.ts +6 -6
  74. package/dist/server-fns/admin-roles/list.d.ts +3 -1
  75. package/dist/server-fns/admin-roles/list.js +4 -1
  76. package/dist/server-fns/admin-roles/reorder.d.ts +3 -1
  77. package/dist/server-fns/admin-roles/reorder.js +4 -1
  78. package/dist/server-fns/admin-roles/update.d.ts +3 -1
  79. package/dist/server-fns/admin-roles/update.js +4 -1
  80. package/dist/server-fns/admin-users/create.d.ts +3 -1
  81. package/dist/server-fns/admin-users/create.js +4 -1
  82. package/dist/server-fns/admin-users/delete.d.ts +3 -1
  83. package/dist/server-fns/admin-users/delete.js +4 -1
  84. package/dist/server-fns/admin-users/disable.d.ts +3 -1
  85. package/dist/server-fns/admin-users/disable.js +4 -1
  86. package/dist/server-fns/admin-users/enable.d.ts +3 -1
  87. package/dist/server-fns/admin-users/enable.js +4 -1
  88. package/dist/server-fns/admin-users/get-user-roles.d.ts +3 -1
  89. package/dist/server-fns/admin-users/get-user-roles.js +4 -1
  90. package/dist/server-fns/admin-users/get.d.ts +3 -1
  91. package/dist/server-fns/admin-users/get.js +4 -1
  92. package/dist/server-fns/admin-users/index.d.ts +10 -10
  93. package/dist/server-fns/admin-users/list.d.ts +3 -1
  94. package/dist/server-fns/admin-users/list.js +4 -1
  95. package/dist/server-fns/admin-users/set-password.d.ts +3 -1
  96. package/dist/server-fns/admin-users/set-password.js +4 -1
  97. package/dist/server-fns/admin-users/set-user-roles.d.ts +3 -1
  98. package/dist/server-fns/admin-users/set-user-roles.js +4 -1
  99. package/dist/server-fns/admin-users/update.d.ts +3 -1
  100. package/dist/server-fns/admin-users/update.js +4 -1
  101. package/dist/server-fns/ai/execute.d.ts +3 -1
  102. package/dist/server-fns/ai/execute.js +4 -1
  103. package/dist/server-fns/ai/index.d.ts +1 -1
  104. package/dist/server-fns/analytics/maintenance.d.ts +6 -2
  105. package/dist/server-fns/analytics/maintenance.js +7 -2
  106. package/dist/server-fns/analytics/queries.d.ts +18 -6
  107. package/dist/server-fns/analytics/queries.js +19 -6
  108. package/dist/server-fns/auth/current-user.d.ts +1 -0
  109. package/dist/server-fns/auth/current-user.js +5 -2
  110. package/dist/server-fns/auth/renew.d.ts +13 -0
  111. package/dist/server-fns/auth/renew.js +45 -0
  112. package/dist/server-fns/auth/sign-in.d.ts +2 -1
  113. package/dist/server-fns/auth/sign-in.js +88 -17
  114. package/dist/server-fns/auth/sign-out.d.ts +3 -1
  115. package/dist/server-fns/auth/sign-out.js +38 -6
  116. package/dist/server-fns/collections/audit.d.ts +3 -1
  117. package/dist/server-fns/collections/audit.js +4 -1
  118. package/dist/server-fns/collections/copy-to-locale.d.ts +5 -1
  119. package/dist/server-fns/collections/copy-to-locale.js +7 -2
  120. package/dist/server-fns/collections/create.d.ts +4 -1
  121. package/dist/server-fns/collections/create.js +8 -3
  122. package/dist/server-fns/collections/delete-locale.d.ts +5 -1
  123. package/dist/server-fns/collections/delete-locale.js +7 -2
  124. package/dist/server-fns/collections/delete-outcome.d.ts +7 -3
  125. package/dist/server-fns/collections/delete-outcome.js +4 -0
  126. package/dist/server-fns/collections/delete.d.ts +5 -1
  127. package/dist/server-fns/collections/delete.js +8 -3
  128. package/dist/server-fns/collections/duplicate.d.ts +5 -1
  129. package/dist/server-fns/collections/duplicate.js +8 -3
  130. package/dist/server-fns/collections/get.d.ts +1 -0
  131. package/dist/server-fns/collections/get.js +12 -6
  132. package/dist/server-fns/collections/history.d.ts +3 -1
  133. package/dist/server-fns/collections/history.js +4 -1
  134. package/dist/server-fns/collections/index.d.ts +21 -21
  135. package/dist/server-fns/collections/list.d.ts +3 -1
  136. package/dist/server-fns/collections/list.js +15 -4
  137. package/dist/server-fns/collections/reindex.d.ts +3 -1
  138. package/dist/server-fns/collections/reindex.js +4 -1
  139. package/dist/server-fns/collections/reorder.d.ts +10 -2
  140. package/dist/server-fns/collections/reorder.js +23 -76
  141. package/dist/server-fns/collections/restore-version.d.ts +6 -1
  142. package/dist/server-fns/collections/restore-version.js +8 -2
  143. package/dist/server-fns/collections/save-outcome.d.ts +1 -0
  144. package/dist/server-fns/collections/save-outcome.js +1 -0
  145. package/dist/server-fns/collections/scheduled-publication.d.ts +64 -7
  146. package/dist/server-fns/collections/scheduled-publication.js +42 -21
  147. package/dist/server-fns/collections/set-list-view-preference.d.ts +3 -1
  148. package/dist/server-fns/collections/set-list-view-preference.js +4 -1
  149. package/dist/server-fns/collections/stats.js +4 -1
  150. package/dist/server-fns/collections/status.d.ts +14 -2
  151. package/dist/server-fns/collections/status.js +18 -5
  152. package/dist/server-fns/collections/tree-mutation.d.ts +7 -6
  153. package/dist/server-fns/collections/tree-mutation.js +17 -5
  154. package/dist/server-fns/collections/tree.d.ts +29 -6
  155. package/dist/server-fns/collections/tree.js +32 -17
  156. package/dist/server-fns/collections/update.d.ts +18 -6
  157. package/dist/server-fns/collections/update.js +39 -20
  158. package/dist/server-fns/collections/upload.d.ts +11 -1
  159. package/dist/server-fns/collections/upload.js +6 -2
  160. package/dist/server-fns/document-mutation-errors.d.ts +5 -0
  161. package/dist/server-fns/document-mutation-errors.js +50 -0
  162. package/dist/server-fns/editable-revision.d.ts +9 -0
  163. package/dist/server-fns/editable-revision.js +11 -0
  164. package/dist/server-fns/i18n/set-locale.d.ts +3 -1
  165. package/dist/server-fns/i18n/set-locale.js +18 -3
  166. package/dist/server-fns/preview/disable.d.ts +3 -1
  167. package/dist/server-fns/preview/disable.js +4 -1
  168. package/dist/server-fns/preview/enable.d.ts +3 -1
  169. package/dist/server-fns/preview/enable.js +4 -1
  170. package/dist/server-fns/singleton-document-read.d.ts +1 -1
  171. package/dist/server-fns/singleton-document-read.js +7 -3
  172. package/dist/server-fns/singletons/cancel-scheduled-publish.d.ts +9 -2
  173. package/dist/server-fns/singletons/cancel-scheduled-publish.js +7 -1
  174. package/dist/server-fns/singletons/change-status.d.ts +5 -1
  175. package/dist/server-fns/singletons/change-status.js +7 -1
  176. package/dist/server-fns/singletons/confirm-scheduled-publish.d.ts +8 -2
  177. package/dist/server-fns/singletons/confirm-scheduled-publish.js +7 -2
  178. package/dist/server-fns/singletons/copy-to-locale.d.ts +5 -1
  179. package/dist/server-fns/singletons/copy-to-locale.js +7 -2
  180. package/dist/server-fns/singletons/find-by-version.d.ts +3 -1
  181. package/dist/server-fns/singletons/find-by-version.js +4 -1
  182. package/dist/server-fns/singletons/get-scheduled-publish.d.ts +3 -1
  183. package/dist/server-fns/singletons/get-scheduled-publish.js +4 -1
  184. package/dist/server-fns/singletons/get.d.ts +6 -4
  185. package/dist/server-fns/singletons/get.js +4 -1
  186. package/dist/server-fns/singletons/history.d.ts +3 -1
  187. package/dist/server-fns/singletons/history.js +4 -1
  188. package/dist/server-fns/singletons/restore-version.d.ts +5 -1
  189. package/dist/server-fns/singletons/restore-version.js +9 -3
  190. package/dist/server-fns/singletons/schedule-publish.d.ts +8 -2
  191. package/dist/server-fns/singletons/schedule-publish.js +7 -2
  192. package/dist/server-fns/singletons/unpublish.d.ts +5 -1
  193. package/dist/server-fns/singletons/unpublish.js +7 -1
  194. package/dist/server-fns/singletons/update.d.ts +6 -4
  195. package/dist/server-fns/singletons/update.js +8 -4
  196. package/dist/server-fns/structural-receipt.d.ts +4 -0
  197. package/dist/server-fns/structural-receipt.js +31 -0
  198. package/package.json +20 -20
  199. package/src/admin-shell/chrome/admin-app-bar.tsx +11 -6
  200. package/src/admin-shell/chrome/session-change-boundary.test.tsx +78 -0
  201. package/src/admin-shell/chrome/session-change-boundary.tsx +94 -0
  202. package/src/admin-shell/chrome/sign-in-page.tsx +68 -3
  203. package/src/admin-shell/collections/edit.tsx +173 -54
  204. package/src/admin-shell/collections/history.tsx +11 -2
  205. package/src/admin-shell/collections/list.tsx +22 -6
  206. package/src/admin-shell/collections/restore-version-modal.tsx +12 -3
  207. package/src/admin-shell/collections/save-navigation.test.tsx +156 -8
  208. package/src/admin-shell/collections/tree-list.tsx +16 -6
  209. package/src/admin-shell/collections/version-history.tsx +13 -3
  210. package/src/admin-shell/document-mutation-notice.tsx +45 -0
  211. package/src/admin-shell/document-mutation-state.test.tsx +146 -0
  212. package/src/admin-shell/document-mutation-state.ts +95 -0
  213. package/src/admin-shell/scheduled-publications/list.tsx +16 -6
  214. package/src/admin-shell/singletons/history.test.tsx +3 -1
  215. package/src/admin-shell/singletons/history.tsx +10 -2
  216. package/src/admin-shell/singletons/view.test.tsx +59 -18
  217. package/src/admin-shell/singletons/view.tsx +110 -29
  218. package/src/integrations/byline-admin-services.ts +7 -1
  219. package/src/integrations/byline-field-services.ts +2 -3
  220. package/src/integrations/client-ip.test.node.ts +45 -0
  221. package/src/integrations/client-ip.ts +54 -0
  222. package/src/integrations/framework-fetch-shape.test.node.ts +56 -0
  223. package/src/integrations/session-client.test.node.ts +121 -0
  224. package/src/integrations/session-coordination.test.node.ts +77 -0
  225. package/src/integrations/session-coordination.ts +96 -0
  226. package/src/integrations/session-middleware.test.node.ts +87 -0
  227. package/src/integrations/session-middleware.ts +107 -0
  228. package/src/integrations/session-request-kind.ts +18 -0
  229. package/src/integrations/session-request-middleware.ts +17 -0
  230. package/src/integrations/sign-in-body.test.node.ts +59 -0
  231. package/src/integrations/sign-in-body.ts +50 -0
  232. package/src/integrations/sign-in-middleware.ts +24 -0
  233. package/src/routes/create-admin-layout-route.tsx +48 -30
  234. package/src/routes/create-collection-edit-route.tsx +1 -0
  235. package/src/routes/create-collection-list-route.tsx +13 -2
  236. package/src/routes/create-sign-in-route.tsx +10 -2
  237. package/src/routes/create-singleton-route.test.node.ts +10 -2
  238. package/src/routes/create-singleton-route.tsx +6 -4
  239. package/src/server-fns/admin-account/change-password.test.node.ts +69 -0
  240. package/src/server-fns/admin-account/change-password.ts +13 -2
  241. package/src/server-fns/admin-account/get.ts +2 -0
  242. package/src/server-fns/admin-account/update.ts +2 -0
  243. package/src/server-fns/admin-activity/get.ts +2 -0
  244. package/src/server-fns/admin-permissions/get-role-abilities.ts +2 -0
  245. package/src/server-fns/admin-permissions/list-registered.ts +5 -4
  246. package/src/server-fns/admin-permissions/set-role-abilities.ts +2 -0
  247. package/src/server-fns/admin-permissions/who-has.ts +2 -0
  248. package/src/server-fns/admin-roles/create.ts +2 -0
  249. package/src/server-fns/admin-roles/delete.ts +2 -0
  250. package/src/server-fns/admin-roles/get.ts +2 -0
  251. package/src/server-fns/admin-roles/list.ts +5 -4
  252. package/src/server-fns/admin-roles/reorder.ts +2 -0
  253. package/src/server-fns/admin-roles/update.ts +2 -0
  254. package/src/server-fns/admin-users/create.ts +2 -0
  255. package/src/server-fns/admin-users/delete.ts +2 -0
  256. package/src/server-fns/admin-users/disable.ts +2 -0
  257. package/src/server-fns/admin-users/enable.ts +2 -0
  258. package/src/server-fns/admin-users/get-user-roles.ts +2 -0
  259. package/src/server-fns/admin-users/get.ts +2 -0
  260. package/src/server-fns/admin-users/list.ts +2 -0
  261. package/src/server-fns/admin-users/set-password.ts +2 -0
  262. package/src/server-fns/admin-users/set-user-roles.ts +2 -0
  263. package/src/server-fns/admin-users/update.ts +2 -0
  264. package/src/server-fns/ai/execute.ts +3 -0
  265. package/src/server-fns/analytics/maintenance.ts +4 -0
  266. package/src/server-fns/analytics/queries.ts +14 -8
  267. package/src/server-fns/auth/current-user.ts +7 -3
  268. package/src/server-fns/auth/renew.test.node.ts +85 -0
  269. package/src/server-fns/auth/renew.ts +54 -0
  270. package/src/server-fns/auth/sign-in.test.node.ts +114 -0
  271. package/src/server-fns/auth/sign-in.ts +80 -27
  272. package/src/server-fns/auth/sign-out.test.node.ts +85 -0
  273. package/src/server-fns/auth/sign-out.ts +44 -19
  274. package/src/server-fns/collections/audit.ts +2 -0
  275. package/src/server-fns/collections/copy-to-locale.ts +43 -36
  276. package/src/server-fns/collections/create.ts +56 -50
  277. package/src/server-fns/collections/delete-locale.ts +43 -32
  278. package/src/server-fns/collections/delete-outcome.test.node.ts +16 -0
  279. package/src/server-fns/collections/delete-outcome.ts +27 -16
  280. package/src/server-fns/collections/delete.ts +46 -38
  281. package/src/server-fns/collections/duplicate.ts +40 -32
  282. package/src/server-fns/collections/get.ts +11 -4
  283. package/src/server-fns/collections/history.ts +2 -0
  284. package/src/server-fns/collections/list.ts +10 -3
  285. package/src/server-fns/collections/reindex.ts +2 -0
  286. package/src/server-fns/collections/reorder.ts +34 -124
  287. package/src/server-fns/collections/restore-version.ts +49 -39
  288. package/src/server-fns/collections/revision-input.test.node.ts +122 -0
  289. package/src/server-fns/collections/save-outcome.test.node.ts +3 -0
  290. package/src/server-fns/collections/save-outcome.ts +2 -0
  291. package/src/server-fns/collections/scheduled-publication.ts +149 -115
  292. package/src/server-fns/collections/set-list-view-preference.ts +2 -0
  293. package/src/server-fns/collections/stats.ts +2 -0
  294. package/src/server-fns/collections/status.ts +89 -64
  295. package/src/server-fns/collections/tree-mutation.test.node.ts +87 -29
  296. package/src/server-fns/collections/tree-mutation.ts +28 -11
  297. package/src/server-fns/collections/tree.ts +156 -122
  298. package/src/server-fns/collections/update.ts +113 -86
  299. package/src/server-fns/collections/upload-singleton.test.node.ts +10 -7
  300. package/src/server-fns/collections/upload.ts +91 -77
  301. package/src/server-fns/document-mutation-errors.ts +67 -0
  302. package/src/server-fns/document-mutation-transport.test.node.ts +211 -0
  303. package/src/server-fns/editable-revision.test.node.ts +23 -0
  304. package/src/server-fns/editable-revision.ts +18 -0
  305. package/src/server-fns/i18n/set-locale.ts +19 -3
  306. package/src/server-fns/preview/disable.ts +8 -4
  307. package/src/server-fns/preview/enable.ts +9 -5
  308. package/src/server-fns/singleton-document-read.ts +7 -3
  309. package/src/server-fns/singletons/cancel-scheduled-publish.ts +12 -3
  310. package/src/server-fns/singletons/change-status.ts +12 -3
  311. package/src/server-fns/singletons/confirm-scheduled-publish.ts +14 -6
  312. package/src/server-fns/singletons/copy-to-locale.ts +22 -15
  313. package/src/server-fns/singletons/find-by-version.ts +2 -0
  314. package/src/server-fns/singletons/get-scheduled-publish.ts +2 -0
  315. package/src/server-fns/singletons/get.ts +5 -3
  316. package/src/server-fns/singletons/history.ts +2 -0
  317. package/src/server-fns/singletons/restore-version.ts +19 -12
  318. package/src/server-fns/singletons/schedule-publish.ts +20 -7
  319. package/src/server-fns/singletons/singletons.test.node.ts +69 -25
  320. package/src/server-fns/singletons/unpublish.ts +12 -3
  321. package/src/server-fns/singletons/update.ts +23 -16
  322. package/src/server-fns/structural-receipt.ts +41 -0
@@ -18,13 +18,21 @@
18
18
  */
19
19
 
20
20
  import { createServerFn } from '@tanstack/react-start'
21
- import { getRequestHeader } from '@tanstack/react-start/server'
22
-
23
- import { setSessionCookies } from '@byline/client/server'
21
+ import { getRequest, getRequestHeader } from '@tanstack/react-start/server'
22
+
23
+ import type { SignInAdmission } from '@byline/auth'
24
+ import {
25
+ readAccessTokenCookie,
26
+ readRefreshTokenCookie,
27
+ setSessionCookies,
28
+ } from '@byline/client/server'
24
29
  import { getServerConfig } from '@byline/core'
30
+ import { passwordSignInSchema } from '@byline/core/validation'
25
31
 
26
32
  import { readAdminLocaleCookie } from '../../i18n/locale-cookie.js'
27
33
  import { bylineCore } from '../../integrations/byline-core.js'
34
+ import { normalizeClientIp } from '../../integrations/client-ip.js'
35
+ import { wasSignInBodyChecked } from '../../integrations/sign-in-body.js'
28
36
 
29
37
  export interface SignInInput {
30
38
  email: string
@@ -33,17 +41,14 @@ export interface SignInInput {
33
41
 
34
42
  export interface SignInResult {
35
43
  userId: string
44
+ sessionId: string
36
45
  }
37
46
 
38
47
  export const adminSignIn = createServerFn({ method: 'POST' })
39
- .validator((input: SignInInput) => {
40
- if (typeof input?.email !== 'string' || input.email.length === 0) {
41
- throw new Error('email is required')
42
- }
43
- if (typeof input?.password !== 'string' || input.password.length === 0) {
44
- throw new Error('password is required')
45
- }
46
- return { email: input.email, password: input.password }
48
+ .validator((input: unknown) => {
49
+ const parsed = passwordSignInSchema.safeParse(input)
50
+ if (!parsed.success) throw new Response('Invalid sign-in input', { status: 400 })
51
+ return parsed.data
47
52
  })
48
53
  .handler(async ({ data }): Promise<SignInResult> => {
49
54
  const provider = getServerConfig().sessionProvider
@@ -51,21 +56,69 @@ export const adminSignIn = createServerFn({ method: 'POST' })
51
56
  throw new Error('no sessionProvider configured')
52
57
  }
53
58
 
54
- // TanStack Start doesn't currently expose the raw request IP in a
55
- // cross-runtime way; pass what we can observe and leave the ip field
56
- // unset. Operators who need accurate client IPs for refresh-token
57
- // provenance will typically run behind a reverse proxy that stamps
58
- // `x-forwarded-for` — revisit when Phase 5 sees real deployments.
59
- const userAgent = getRequestHeader('user-agent') ?? undefined
60
- const forwardedFor = getRequestHeader('x-forwarded-for') ?? undefined
61
- const ip = forwardedFor?.split(',')[0]?.trim() || undefined
62
-
63
- const result = await provider.signInWithPassword({
64
- email: data.email,
65
- password: data.password,
66
- userAgent,
67
- ip,
68
- })
59
+ const request = getRequest()
60
+ const protection = getServerConfig().passwordSignIn
61
+ if (!wasSignInBodyChecked(request)) {
62
+ throw new Response('Password sign-in request middleware is not configured', { status: 503 })
63
+ }
64
+ if (!protection) {
65
+ throw new Response('Password sign-in protection is not configured', { status: 503 })
66
+ }
67
+ const userAgent = getRequestHeader('user-agent')?.slice(0, 512)
68
+ let ip: string | null
69
+ try {
70
+ ip = normalizeClientIp(await protection.resolveClientIp(request))
71
+ } catch {
72
+ throw new Response('Client address unavailable', { status: 503 })
73
+ }
74
+ if (!ip) throw new Response('Client address unavailable', { status: 503 })
75
+ const tooMany = (seconds: number) =>
76
+ new Response('Too many sign-in attempts', {
77
+ status: 429,
78
+ headers: { 'Retry-After': String(seconds), 'Cache-Control': 'no-store' },
79
+ })
80
+ let release: (() => void) | null
81
+ try {
82
+ release = await protection.limiter.acquire()
83
+ } catch {
84
+ throw new Response('Sign-in temporarily unavailable', { status: 503 })
85
+ }
86
+ if (!release) throw tooMany(1)
87
+ const recordResult = (outcome: 'success' | 'failure') => {
88
+ try {
89
+ protection.limiter.recordResult?.({ email: data.email, ip }, outcome)
90
+ } catch {
91
+ /* Telemetry cannot change authentication outcomes. */
92
+ }
93
+ }
94
+ const result = await (async () => {
95
+ try {
96
+ let admission: SignInAdmission
97
+ try {
98
+ admission = await protection.limiter.consume({ email: data.email, ip })
99
+ } catch {
100
+ throw new Response('Sign-in temporarily unavailable', { status: 503 })
101
+ }
102
+ if (!admission.allowed) throw tooMany(admission.retryAfterSeconds)
103
+ try {
104
+ const result = await provider.signInWithPassword({
105
+ previousAccessToken: readAccessTokenCookie(),
106
+ previousRefreshToken: readRefreshTokenCookie(),
107
+ email: data.email,
108
+ password: data.password,
109
+ userAgent,
110
+ ip,
111
+ })
112
+ recordResult('success')
113
+ return result
114
+ } catch (error) {
115
+ recordResult('failure')
116
+ throw error
117
+ }
118
+ } finally {
119
+ release()
120
+ }
121
+ })()
69
122
 
70
123
  setSessionCookies(result)
71
124
 
@@ -82,7 +135,7 @@ export const adminSignIn = createServerFn({ method: 'POST' })
82
135
  // Swallow — locale sync is not load-bearing for the sign-in flow.
83
136
  }
84
137
 
85
- return { userId: result.actor.id }
138
+ return { userId: result.actor.id, sessionId: result.sessionId }
86
139
  })
87
140
 
88
141
  /**
@@ -0,0 +1,85 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
10
+
11
+ const mocks = vi.hoisted(() => ({
12
+ access: 'access' as string | undefined,
13
+ refresh: 'refresh' as string | undefined,
14
+ revoke: vi.fn(),
15
+ clear: vi.fn(),
16
+ clearPreview: vi.fn(),
17
+ }))
18
+ vi.mock('@tanstack/react-start', () => ({
19
+ createMiddleware: () => ({ client: () => ({}) }),
20
+ createServerFn: () => ({ middleware: () => ({ handler: (handler: any) => handler }) }),
21
+ }))
22
+ vi.mock('@byline/core', () => ({
23
+ getServerConfig: () => ({ sessionProvider: { revokeSession: mocks.revoke } }),
24
+ }))
25
+ vi.mock('@byline/client/server', () => ({
26
+ readAccessTokenCookie: () => mocks.access,
27
+ readRefreshTokenCookie: () => mocks.refresh,
28
+ clearSessionCookies: mocks.clear,
29
+ clearPreviewCookie: mocks.clearPreview,
30
+ }))
31
+
32
+ import { adminSignOut } from './sign-out.js'
33
+
34
+ const call = adminSignOut as unknown as (args: {
35
+ context: { expectedSessionId?: string }
36
+ }) => Promise<unknown>
37
+ beforeEach(() => {
38
+ vi.resetAllMocks()
39
+ mocks.access = 'access'
40
+ mocks.refresh = 'refresh'
41
+ })
42
+
43
+ describe('confirmed logout', () => {
44
+ it('revokes with both observed credentials before clearing cookies', async () => {
45
+ mocks.revoke.mockImplementation(async () => {
46
+ expect(mocks.clear).not.toHaveBeenCalled()
47
+ })
48
+ await expect(call({ context: { expectedSessionId: 'A' } })).resolves.toEqual({ status: 'ok' })
49
+ expect(mocks.revoke).toHaveBeenCalledWith({
50
+ accessToken: 'access',
51
+ refreshToken: 'refresh',
52
+ expectedSessionId: 'A',
53
+ })
54
+ expect(mocks.clear).toHaveBeenCalledOnce()
55
+ })
56
+ it('still requests revocation when only the access cookie remains', async () => {
57
+ mocks.refresh = undefined
58
+ await call({ context: { expectedSessionId: 'A' } })
59
+ expect(mocks.revoke).toHaveBeenCalledWith({
60
+ accessToken: 'access',
61
+ refreshToken: undefined,
62
+ expectedSessionId: 'A',
63
+ })
64
+ })
65
+ it('does not clear credentials or report success when revocation fails', async () => {
66
+ mocks.revoke.mockRejectedValue(new Error('database unavailable'))
67
+ await expect(call({ context: { expectedSessionId: 'A' } })).rejects.toThrow(
68
+ 'database unavailable'
69
+ )
70
+ expect(mocks.clear).not.toHaveBeenCalled()
71
+ expect(mocks.clearPreview).not.toHaveBeenCalled()
72
+ })
73
+ it('requires the page identity before revoking any observed credential', async () => {
74
+ await expect(call({ context: {} })).rejects.toMatchObject({ code: 'ERR_SESSION_CHANGED' })
75
+ expect(mocks.revoke).not.toHaveBeenCalled()
76
+ expect(mocks.clear).not.toHaveBeenCalled()
77
+ })
78
+ it('is idempotent when neither credential is present', async () => {
79
+ mocks.access = undefined
80
+ mocks.refresh = undefined
81
+ await expect(call({ context: {} })).resolves.toEqual({ status: 'ok' })
82
+ expect(mocks.revoke).not.toHaveBeenCalled()
83
+ expect(mocks.clear).toHaveBeenCalledOnce()
84
+ })
85
+ })
@@ -9,9 +9,9 @@
9
9
  /**
10
10
  * Admin sign-out server function.
11
11
  *
12
- * Revokes the current refresh token (so a stolen copy cannot be reused),
12
+ * Revokes the login identified by either credential (so a stolen copy cannot be reused),
13
13
  * clears both session cookies, and clears the preview-mode cookie.
14
- * Idempotent — if the caller already lacks a refresh cookie, we still
14
+ * Idempotent — if the caller already lacks both credential cookies, we still
15
15
  * clear whatever's there and return successfully.
16
16
  *
17
17
  * Note: clearing `byline_preview` here is hygiene, not a security
@@ -21,32 +21,57 @@
21
21
  * Clearing it simply means the next sign-in starts in non-preview mode.
22
22
  */
23
23
 
24
- import { createServerFn } from '@tanstack/react-start'
24
+ import { createMiddleware, createServerFn } from '@tanstack/react-start'
25
25
 
26
+ import { ERR_SESSION_CHANGED } from '@byline/auth'
26
27
  import {
27
28
  clearPreviewCookie,
28
29
  clearSessionCookies,
30
+ readAccessTokenCookie,
29
31
  readRefreshTokenCookie,
30
32
  } from '@byline/client/server'
31
33
  import { getServerConfig } from '@byline/core'
32
34
 
33
- export const adminSignOut = createServerFn({ method: 'POST' }).handler(async () => {
34
- const provider = getServerConfig().sessionProvider
35
- const refreshToken = readRefreshTokenCookie()
35
+ import {
36
+ coordinateAuthAction,
37
+ flagSessionChanged,
38
+ notifySessionAction,
39
+ sessionSnapshot,
40
+ } from '../../integrations/session-coordination.js'
36
41
 
37
- if (provider && refreshToken) {
38
- // Best-effort revoke — `revokeSession` itself is idempotent for
39
- // unknown/already-revoked tokens, so failures here would almost
40
- // certainly be transport-level. Swallow and continue: the cookies
41
- // get cleared regardless, so the caller's session ends.
42
+ const signOutBoundary = createMiddleware({ type: 'function' }).client(async ({ next }) => {
43
+ const snapshot = sessionSnapshot()
44
+ return coordinateAuthAction(async () => {
42
45
  try {
43
- await provider.revokeSession(refreshToken)
44
- } catch {
45
- // no-op
46
+ const result = await next({ sendContext: { expectedSessionId: snapshot.sessionId } })
47
+ notifySessionAction()
48
+ return result
49
+ } catch (error) {
50
+ if ((error as { code?: string })?.code === 'ERR_SESSION_CHANGED') flagSessionChanged()
51
+ throw error
46
52
  }
47
- }
48
-
49
- clearSessionCookies()
50
- clearPreviewCookie()
51
- return { status: 'ok' as const }
53
+ })
52
54
  })
55
+
56
+ export const adminSignOut = createServerFn({ method: 'POST' })
57
+ .middleware([signOutBoundary])
58
+ .handler(async ({ context }) => {
59
+ const provider = getServerConfig().sessionProvider
60
+ const refreshToken = readRefreshTokenCookie()
61
+ const accessToken = readAccessTokenCookie()
62
+
63
+ if (refreshToken || accessToken) {
64
+ if (!provider) throw new Error('no sessionProvider configured')
65
+ if (!context.expectedSessionId)
66
+ throw ERR_SESSION_CHANGED({ message: 'logout requires the page login identity' })
67
+ await provider.revokeSession({
68
+ refreshToken,
69
+ accessToken,
70
+ expectedSessionId: context.expectedSessionId,
71
+ })
72
+ }
73
+
74
+ clearSessionCookies()
75
+ clearPreviewCookie()
76
+ return { status: 'ok' as const }
77
+ })
@@ -12,6 +12,7 @@ import { getAdminBylineClient } from '@byline/client/server'
12
12
  import { ERR_NOT_FOUND, getLogger } from '@byline/core'
13
13
 
14
14
  import { ensureCollection } from '../../integrations/api-utils.js'
15
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
15
16
  import { type ActorLabelMap, resolveActorLabels } from './actors.js'
16
17
 
17
18
  // ---------------------------------------------------------------------------
@@ -49,6 +50,7 @@ export interface AuditLogEntryDto {
49
50
  // ---------------------------------------------------------------------------
50
51
 
51
52
  export const getCollectionDocumentAuditLog = createServerFn({ method: 'GET' })
53
+ .middleware([adminSessionMiddleware])
52
54
  .validator((input: { collection: string; id: string; params?: AuditLogSearchParams }) => input)
53
55
  .handler(async ({ data }) => {
54
56
  const { collection: path, id, params } = data
@@ -14,6 +14,8 @@ import type { CopyToLocaleResult, DocumentLifecycleContext } from '@byline/core/
14
14
  import { copyToLocale } from '@byline/core/services'
15
15
 
16
16
  import { ensureCollection } from '../../integrations/api-utils.js'
17
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
18
+ import { withDocumentMutationErrors } from '../document-mutation-errors.js'
17
19
  import {
18
20
  type CollectionDocumentCommittedHookFailureResponse,
19
21
  toCommittedDocumentHookFailureResponse,
@@ -33,8 +35,10 @@ import {
33
35
  * failures propagate to TanStack Start's transport layer.
34
36
  */
35
37
  export const copyDocumentToLocale = createServerFn({ method: 'POST' })
38
+ .middleware([adminSessionMiddleware])
36
39
  .validator(
37
40
  (input: {
41
+ expectedRevision: number
38
42
  collection: string
39
43
  id: string
40
44
  sourceLocale: string
@@ -43,43 +47,46 @@ export const copyDocumentToLocale = createServerFn({ method: 'POST' })
43
47
  }) => input
44
48
  )
45
49
  .handler(
46
- async ({
47
- data: input,
48
- }): Promise<CopyToLocaleResult | CollectionDocumentCommittedHookFailureResponse> => {
49
- const { collection: path, id, sourceLocale, targetLocale, overwrite } = input
50
- const logger = getLogger()
51
- const config = await ensureCollection(path)
52
- if (!config) {
53
- throw ERR_NOT_FOUND({
54
- message: 'Collection not found',
55
- details: { collectionPath: path },
56
- }).log(logger)
57
- }
50
+ withDocumentMutationErrors(
51
+ async ({
52
+ data: input,
53
+ }): Promise<CopyToLocaleResult | CollectionDocumentCommittedHookFailureResponse> => {
54
+ const { collection: path, id, sourceLocale, targetLocale, overwrite } = input
55
+ const logger = getLogger()
56
+ const config = await ensureCollection(path)
57
+ if (!config) {
58
+ throw ERR_NOT_FOUND({
59
+ message: 'Collection not found',
60
+ details: { collectionPath: path },
61
+ }).log(logger)
62
+ }
58
63
 
59
- const serverConfig = getServerConfig()
60
- const ctx: DocumentLifecycleContext = {
61
- db: serverConfig.db,
62
- definition: config.definition,
63
- collectionId: config.collection.id,
64
- collectionVersion: config.collection.version,
65
- collectionPath: path,
66
- logger,
67
- defaultLocale: serverConfig.i18n.content.defaultLocale,
68
- slugifier: serverConfig.slugifier,
69
- requestContext: await getAdminRequestContext(),
70
- }
64
+ const serverConfig = getServerConfig()
65
+ const ctx: DocumentLifecycleContext = {
66
+ db: serverConfig.db,
67
+ definition: config.definition,
68
+ collectionId: config.collection.id,
69
+ collectionVersion: config.collection.version,
70
+ collectionPath: path,
71
+ logger,
72
+ defaultLocale: serverConfig.i18n.content.defaultLocale,
73
+ slugifier: serverConfig.slugifier,
74
+ requestContext: await getAdminRequestContext(),
75
+ }
71
76
 
72
- try {
73
- return await copyToLocale(ctx, {
74
- documentId: id,
75
- sourceLocale,
76
- targetLocale,
77
- overwrite,
78
- })
79
- } catch (error) {
80
- const committedFailure = toCommittedDocumentHookFailureResponse(error)
81
- if (committedFailure != null) return committedFailure
82
- throw error
77
+ try {
78
+ return await copyToLocale(ctx, {
79
+ documentId: id,
80
+ expectedRevision: input.expectedRevision,
81
+ sourceLocale,
82
+ targetLocale,
83
+ overwrite,
84
+ })
85
+ } catch (error) {
86
+ const committedFailure = toCommittedDocumentHookFailureResponse(error)
87
+ if (committedFailure != null) return committedFailure
88
+ throw error
89
+ }
83
90
  }
84
- }
91
+ )
85
92
  )
@@ -14,6 +14,8 @@ import type { CreateDocumentResult, DocumentLifecycleContext } from '@byline/cor
14
14
  import { createDocument } from '@byline/core/services'
15
15
 
16
16
  import { ensureCollection } from '../../integrations/api-utils.js'
17
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
18
+ import { withDocumentMutationErrors } from '../document-mutation-errors.js'
17
19
  import { toCommittedDocumentHookFailureResponse } from './save-outcome.js'
18
20
 
19
21
  // ---------------------------------------------------------------------------
@@ -21,6 +23,7 @@ import { toCommittedDocumentHookFailureResponse } from './save-outcome.js'
21
23
  // ---------------------------------------------------------------------------
22
24
 
23
25
  export const createCollectionDocument = createServerFn({ method: 'POST' })
26
+ .middleware([adminSessionMiddleware])
24
27
  .validator(
25
28
  (input: {
26
29
  collection: string
@@ -30,56 +33,59 @@ export const createCollectionDocument = createServerFn({ method: 'POST' })
30
33
  availableLocales?: string[]
31
34
  }) => input
32
35
  )
33
- .handler(async ({ data: input }) => {
34
- const {
35
- collection: path,
36
- data: documentData,
37
- locale,
38
- path: explicitPath,
39
- availableLocales,
40
- } = input
41
- const logger = getLogger()
42
- const config = await ensureCollection(path)
43
- if (!config) {
44
- throw ERR_NOT_FOUND({
45
- message: 'Collection not found',
46
- details: { collectionPath: path },
47
- }).log(logger)
48
- }
49
-
50
- const serverConfig = getServerConfig()
51
- const ctx: DocumentLifecycleContext = {
52
- db: serverConfig.db,
53
- definition: config.definition,
54
- collectionId: config.collection.id,
55
- collectionVersion: config.collection.version,
56
- collectionPath: path,
57
- logger,
58
- defaultLocale: serverConfig.i18n.content.defaultLocale,
59
- slugifier: serverConfig.slugifier,
60
- requestContext: await getAdminRequestContext(),
61
- }
62
-
63
- let result: CreateDocumentResult
64
- try {
65
- result = await createDocument(ctx, {
66
- data: structuredClone(documentData),
67
- status: documentData.status,
68
- locale: locale ?? serverConfig.i18n.content.defaultLocale,
36
+ .handler(
37
+ withDocumentMutationErrors(async ({ data: input }) => {
38
+ const {
39
+ collection: path,
40
+ data: documentData,
41
+ locale,
69
42
  path: explicitPath,
70
43
  availableLocales,
71
- })
72
- } catch (error) {
73
- const committedFailure = toCommittedDocumentHookFailureResponse(error)
74
- if (committedFailure != null) return committedFailure
75
- throw error
76
- }
44
+ } = input
45
+ const logger = getLogger()
46
+ const config = await ensureCollection(path)
47
+ if (!config) {
48
+ throw ERR_NOT_FOUND({
49
+ message: 'Collection not found',
50
+ details: { collectionPath: path },
51
+ }).log(logger)
52
+ }
53
+
54
+ const serverConfig = getServerConfig()
55
+ const ctx: DocumentLifecycleContext = {
56
+ db: serverConfig.db,
57
+ definition: config.definition,
58
+ collectionId: config.collection.id,
59
+ collectionVersion: config.collection.version,
60
+ collectionPath: path,
61
+ logger,
62
+ defaultLocale: serverConfig.i18n.content.defaultLocale,
63
+ slugifier: serverConfig.slugifier,
64
+ requestContext: await getAdminRequestContext(),
65
+ }
77
66
 
78
- // The new document's id is returned so the create view can navigate
79
- // straight to the edit view (create → edit).
80
- return {
81
- status: 'ok' as const,
82
- documentId: result.documentId,
83
- documentVersionId: result.documentVersionId,
84
- }
85
- })
67
+ let result: CreateDocumentResult
68
+ try {
69
+ result = await createDocument(ctx, {
70
+ data: structuredClone(documentData),
71
+ status: documentData.status,
72
+ locale: locale ?? serverConfig.i18n.content.defaultLocale,
73
+ path: explicitPath,
74
+ availableLocales,
75
+ })
76
+ } catch (error) {
77
+ const committedFailure = toCommittedDocumentHookFailureResponse(error)
78
+ if (committedFailure != null) return committedFailure
79
+ throw error
80
+ }
81
+
82
+ // The new document's id is returned so the create view can navigate
83
+ // straight to the edit view (create → edit).
84
+ return {
85
+ status: 'ok' as const,
86
+ documentId: result.documentId,
87
+ documentVersionId: result.documentVersionId,
88
+ revision: result.revision,
89
+ }
90
+ })
91
+ )
@@ -14,6 +14,8 @@ import type { DeleteLocaleResult, DocumentLifecycleContext } from '@byline/core/
14
14
  import { deleteLocale } from '@byline/core/services'
15
15
 
16
16
  import { ensureCollection } from '../../integrations/api-utils.js'
17
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
18
+ import { withDocumentMutationErrors } from '../document-mutation-errors.js'
17
19
  import {
18
20
  type CollectionDocumentCommittedHookFailureResponse,
19
21
  toCommittedDocumentHookFailureResponse,
@@ -34,40 +36,49 @@ import {
34
36
  * transport layer.
35
37
  */
36
38
  export const deleteDocumentLocale = createServerFn({ method: 'POST' })
37
- .validator((input: { collection: string; id: string; locale: string }) => input)
39
+ .middleware([adminSessionMiddleware])
40
+ .validator(
41
+ (input: { expectedRevision: number; collection: string; id: string; locale: string }) => input
42
+ )
38
43
  .handler(
39
- async ({
40
- data: input,
41
- }): Promise<DeleteLocaleResult | CollectionDocumentCommittedHookFailureResponse> => {
42
- const { collection: path, id, locale } = input
43
- const logger = getLogger()
44
- const config = await ensureCollection(path)
45
- if (!config) {
46
- throw ERR_NOT_FOUND({
47
- message: 'Collection not found',
48
- details: { collectionPath: path },
49
- }).log(logger)
50
- }
44
+ withDocumentMutationErrors(
45
+ async ({
46
+ data: input,
47
+ }): Promise<DeleteLocaleResult | CollectionDocumentCommittedHookFailureResponse> => {
48
+ const { collection: path, id, locale } = input
49
+ const logger = getLogger()
50
+ const config = await ensureCollection(path)
51
+ if (!config) {
52
+ throw ERR_NOT_FOUND({
53
+ message: 'Collection not found',
54
+ details: { collectionPath: path },
55
+ }).log(logger)
56
+ }
51
57
 
52
- const serverConfig = getServerConfig()
53
- const ctx: DocumentLifecycleContext = {
54
- db: serverConfig.db,
55
- definition: config.definition,
56
- collectionId: config.collection.id,
57
- collectionVersion: config.collection.version,
58
- collectionPath: path,
59
- logger,
60
- defaultLocale: serverConfig.i18n.content.defaultLocale,
61
- slugifier: serverConfig.slugifier,
62
- requestContext: await getAdminRequestContext(),
63
- }
58
+ const serverConfig = getServerConfig()
59
+ const ctx: DocumentLifecycleContext = {
60
+ db: serverConfig.db,
61
+ definition: config.definition,
62
+ collectionId: config.collection.id,
63
+ collectionVersion: config.collection.version,
64
+ collectionPath: path,
65
+ logger,
66
+ defaultLocale: serverConfig.i18n.content.defaultLocale,
67
+ slugifier: serverConfig.slugifier,
68
+ requestContext: await getAdminRequestContext(),
69
+ }
64
70
 
65
- try {
66
- return await deleteLocale(ctx, { documentId: id, locale })
67
- } catch (error) {
68
- const committedFailure = toCommittedDocumentHookFailureResponse(error)
69
- if (committedFailure != null) return committedFailure
70
- throw error
71
+ try {
72
+ return await deleteLocale(ctx, {
73
+ documentId: id,
74
+ expectedRevision: input.expectedRevision,
75
+ locale,
76
+ })
77
+ } catch (error) {
78
+ const committedFailure = toCommittedDocumentHookFailureResponse(error)
79
+ if (committedFailure != null) return committedFailure
80
+ throw error
81
+ }
71
82
  }
72
- }
83
+ )
73
84
  )