@byline/host-tanstack-start 5.0.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 (252) 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/integrations/byline-admin-services.js +7 -1
  9. package/dist/integrations/client-ip.d.ts +19 -0
  10. package/dist/integrations/client-ip.js +34 -0
  11. package/dist/integrations/session-coordination.d.ts +24 -0
  12. package/dist/integrations/session-coordination.js +85 -0
  13. package/dist/integrations/session-middleware.d.ts +11 -0
  14. package/dist/integrations/session-middleware.js +94 -0
  15. package/dist/integrations/session-request-kind.d.ts +9 -0
  16. package/dist/integrations/session-request-kind.js +10 -0
  17. package/dist/integrations/session-request-middleware.d.ts +9 -0
  18. package/dist/integrations/session-request-middleware.js +7 -0
  19. package/dist/integrations/sign-in-body.d.ts +12 -0
  20. package/dist/integrations/sign-in-body.js +52 -0
  21. package/dist/integrations/sign-in-middleware.d.ts +9 -0
  22. package/dist/integrations/sign-in-middleware.js +10 -0
  23. package/dist/routes/create-admin-layout-route.js +46 -31
  24. package/dist/routes/create-sign-in-route.js +4 -2
  25. package/dist/server-fns/admin-account/change-password.d.ts +3 -1
  26. package/dist/server-fns/admin-account/change-password.js +9 -3
  27. package/dist/server-fns/admin-account/get.d.ts +3 -1
  28. package/dist/server-fns/admin-account/get.js +4 -1
  29. package/dist/server-fns/admin-account/index.d.ts +5 -5
  30. package/dist/server-fns/admin-account/update.d.ts +3 -1
  31. package/dist/server-fns/admin-account/update.js +4 -1
  32. package/dist/server-fns/admin-activity/get.d.ts +3 -1
  33. package/dist/server-fns/admin-activity/get.js +4 -1
  34. package/dist/server-fns/admin-permissions/get-role-abilities.d.ts +3 -1
  35. package/dist/server-fns/admin-permissions/get-role-abilities.js +4 -1
  36. package/dist/server-fns/admin-permissions/index.d.ts +4 -4
  37. package/dist/server-fns/admin-permissions/list-registered.d.ts +3 -1
  38. package/dist/server-fns/admin-permissions/list-registered.js +4 -1
  39. package/dist/server-fns/admin-permissions/set-role-abilities.d.ts +3 -1
  40. package/dist/server-fns/admin-permissions/set-role-abilities.js +4 -1
  41. package/dist/server-fns/admin-permissions/who-has.d.ts +3 -1
  42. package/dist/server-fns/admin-permissions/who-has.js +4 -1
  43. package/dist/server-fns/admin-roles/create.d.ts +3 -1
  44. package/dist/server-fns/admin-roles/create.js +4 -1
  45. package/dist/server-fns/admin-roles/delete.d.ts +3 -1
  46. package/dist/server-fns/admin-roles/delete.js +4 -1
  47. package/dist/server-fns/admin-roles/get.d.ts +3 -1
  48. package/dist/server-fns/admin-roles/get.js +4 -1
  49. package/dist/server-fns/admin-roles/index.d.ts +6 -6
  50. package/dist/server-fns/admin-roles/list.d.ts +3 -1
  51. package/dist/server-fns/admin-roles/list.js +4 -1
  52. package/dist/server-fns/admin-roles/reorder.d.ts +3 -1
  53. package/dist/server-fns/admin-roles/reorder.js +4 -1
  54. package/dist/server-fns/admin-roles/update.d.ts +3 -1
  55. package/dist/server-fns/admin-roles/update.js +4 -1
  56. package/dist/server-fns/admin-users/create.d.ts +3 -1
  57. package/dist/server-fns/admin-users/create.js +4 -1
  58. package/dist/server-fns/admin-users/delete.d.ts +3 -1
  59. package/dist/server-fns/admin-users/delete.js +4 -1
  60. package/dist/server-fns/admin-users/disable.d.ts +3 -1
  61. package/dist/server-fns/admin-users/disable.js +4 -1
  62. package/dist/server-fns/admin-users/enable.d.ts +3 -1
  63. package/dist/server-fns/admin-users/enable.js +4 -1
  64. package/dist/server-fns/admin-users/get-user-roles.d.ts +3 -1
  65. package/dist/server-fns/admin-users/get-user-roles.js +4 -1
  66. package/dist/server-fns/admin-users/get.d.ts +3 -1
  67. package/dist/server-fns/admin-users/get.js +4 -1
  68. package/dist/server-fns/admin-users/index.d.ts +10 -10
  69. package/dist/server-fns/admin-users/list.d.ts +3 -1
  70. package/dist/server-fns/admin-users/list.js +4 -1
  71. package/dist/server-fns/admin-users/set-password.d.ts +3 -1
  72. package/dist/server-fns/admin-users/set-password.js +4 -1
  73. package/dist/server-fns/admin-users/set-user-roles.d.ts +3 -1
  74. package/dist/server-fns/admin-users/set-user-roles.js +4 -1
  75. package/dist/server-fns/admin-users/update.d.ts +3 -1
  76. package/dist/server-fns/admin-users/update.js +4 -1
  77. package/dist/server-fns/ai/execute.d.ts +3 -1
  78. package/dist/server-fns/ai/execute.js +4 -1
  79. package/dist/server-fns/ai/index.d.ts +1 -1
  80. package/dist/server-fns/analytics/maintenance.d.ts +6 -2
  81. package/dist/server-fns/analytics/maintenance.js +7 -2
  82. package/dist/server-fns/analytics/queries.d.ts +18 -6
  83. package/dist/server-fns/analytics/queries.js +19 -6
  84. package/dist/server-fns/auth/current-user.d.ts +1 -0
  85. package/dist/server-fns/auth/current-user.js +5 -2
  86. package/dist/server-fns/auth/renew.d.ts +13 -0
  87. package/dist/server-fns/auth/renew.js +45 -0
  88. package/dist/server-fns/auth/sign-in.d.ts +2 -1
  89. package/dist/server-fns/auth/sign-in.js +88 -17
  90. package/dist/server-fns/auth/sign-out.d.ts +3 -1
  91. package/dist/server-fns/auth/sign-out.js +38 -6
  92. package/dist/server-fns/collections/audit.d.ts +3 -1
  93. package/dist/server-fns/collections/audit.js +4 -1
  94. package/dist/server-fns/collections/copy-to-locale.d.ts +10 -1
  95. package/dist/server-fns/collections/copy-to-locale.js +5 -2
  96. package/dist/server-fns/collections/create.d.ts +10 -1
  97. package/dist/server-fns/collections/create.js +5 -2
  98. package/dist/server-fns/collections/delete-locale.d.ts +10 -1
  99. package/dist/server-fns/collections/delete-locale.js +5 -2
  100. package/dist/server-fns/collections/delete.d.ts +10 -1
  101. package/dist/server-fns/collections/delete.js +5 -2
  102. package/dist/server-fns/collections/duplicate.d.ts +10 -1
  103. package/dist/server-fns/collections/duplicate.js +5 -2
  104. package/dist/server-fns/collections/get.d.ts +7 -0
  105. package/dist/server-fns/collections/get.js +8 -3
  106. package/dist/server-fns/collections/history.d.ts +3 -1
  107. package/dist/server-fns/collections/history.js +4 -1
  108. package/dist/server-fns/collections/index.d.ts +21 -21
  109. package/dist/server-fns/collections/list.d.ts +10 -1
  110. package/dist/server-fns/collections/list.js +5 -2
  111. package/dist/server-fns/collections/reindex.d.ts +3 -1
  112. package/dist/server-fns/collections/reindex.js +4 -1
  113. package/dist/server-fns/collections/reorder.d.ts +10 -1
  114. package/dist/server-fns/collections/reorder.js +5 -2
  115. package/dist/server-fns/collections/restore-version.d.ts +10 -1
  116. package/dist/server-fns/collections/restore-version.js +5 -2
  117. package/dist/server-fns/collections/scheduled-publication.d.ts +22 -5
  118. package/dist/server-fns/collections/scheduled-publication.js +17 -6
  119. package/dist/server-fns/collections/set-list-view-preference.d.ts +3 -1
  120. package/dist/server-fns/collections/set-list-view-preference.js +4 -1
  121. package/dist/server-fns/collections/stats.js +4 -1
  122. package/dist/server-fns/collections/status.d.ts +13 -2
  123. package/dist/server-fns/collections/status.js +8 -3
  124. package/dist/server-fns/collections/tree.d.ts +22 -5
  125. package/dist/server-fns/collections/tree.js +17 -6
  126. package/dist/server-fns/collections/update.d.ts +13 -2
  127. package/dist/server-fns/collections/update.js +8 -3
  128. package/dist/server-fns/collections/upload.d.ts +10 -1
  129. package/dist/server-fns/collections/upload.js +4 -1
  130. package/dist/server-fns/i18n/set-locale.d.ts +3 -1
  131. package/dist/server-fns/i18n/set-locale.js +18 -3
  132. package/dist/server-fns/preview/disable.d.ts +3 -1
  133. package/dist/server-fns/preview/disable.js +4 -1
  134. package/dist/server-fns/preview/enable.d.ts +3 -1
  135. package/dist/server-fns/preview/enable.js +4 -1
  136. package/dist/server-fns/singletons/cancel-scheduled-publish.d.ts +10 -1
  137. package/dist/server-fns/singletons/cancel-scheduled-publish.js +5 -2
  138. package/dist/server-fns/singletons/change-status.d.ts +10 -1
  139. package/dist/server-fns/singletons/change-status.js +5 -2
  140. package/dist/server-fns/singletons/confirm-scheduled-publish.d.ts +10 -1
  141. package/dist/server-fns/singletons/confirm-scheduled-publish.js +5 -2
  142. package/dist/server-fns/singletons/copy-to-locale.d.ts +10 -1
  143. package/dist/server-fns/singletons/copy-to-locale.js +5 -2
  144. package/dist/server-fns/singletons/find-by-version.d.ts +3 -1
  145. package/dist/server-fns/singletons/find-by-version.js +4 -1
  146. package/dist/server-fns/singletons/get-scheduled-publish.d.ts +3 -1
  147. package/dist/server-fns/singletons/get-scheduled-publish.js +4 -1
  148. package/dist/server-fns/singletons/get.d.ts +3 -1
  149. package/dist/server-fns/singletons/get.js +4 -1
  150. package/dist/server-fns/singletons/history.d.ts +3 -1
  151. package/dist/server-fns/singletons/history.js +4 -1
  152. package/dist/server-fns/singletons/restore-version.d.ts +10 -1
  153. package/dist/server-fns/singletons/restore-version.js +5 -2
  154. package/dist/server-fns/singletons/schedule-publish.d.ts +10 -1
  155. package/dist/server-fns/singletons/schedule-publish.js +5 -2
  156. package/dist/server-fns/singletons/unpublish.d.ts +10 -1
  157. package/dist/server-fns/singletons/unpublish.js +5 -2
  158. package/dist/server-fns/singletons/update.d.ts +10 -1
  159. package/dist/server-fns/singletons/update.js +5 -2
  160. package/package.json +19 -19
  161. package/src/admin-shell/chrome/admin-app-bar.tsx +11 -6
  162. package/src/admin-shell/chrome/session-change-boundary.test.tsx +78 -0
  163. package/src/admin-shell/chrome/session-change-boundary.tsx +94 -0
  164. package/src/admin-shell/chrome/sign-in-page.tsx +68 -3
  165. package/src/integrations/byline-admin-services.ts +7 -1
  166. package/src/integrations/client-ip.test.node.ts +45 -0
  167. package/src/integrations/client-ip.ts +54 -0
  168. package/src/integrations/framework-fetch-shape.test.node.ts +56 -0
  169. package/src/integrations/session-client.test.node.ts +121 -0
  170. package/src/integrations/session-coordination.test.node.ts +77 -0
  171. package/src/integrations/session-coordination.ts +96 -0
  172. package/src/integrations/session-middleware.test.node.ts +87 -0
  173. package/src/integrations/session-middleware.ts +107 -0
  174. package/src/integrations/session-request-kind.ts +18 -0
  175. package/src/integrations/session-request-middleware.ts +17 -0
  176. package/src/integrations/sign-in-body.test.node.ts +59 -0
  177. package/src/integrations/sign-in-body.ts +50 -0
  178. package/src/integrations/sign-in-middleware.ts +24 -0
  179. package/src/routes/create-admin-layout-route.tsx +48 -30
  180. package/src/routes/create-sign-in-route.tsx +10 -2
  181. package/src/server-fns/admin-account/change-password.test.node.ts +69 -0
  182. package/src/server-fns/admin-account/change-password.ts +13 -2
  183. package/src/server-fns/admin-account/get.ts +2 -0
  184. package/src/server-fns/admin-account/update.ts +2 -0
  185. package/src/server-fns/admin-activity/get.ts +2 -0
  186. package/src/server-fns/admin-permissions/get-role-abilities.ts +2 -0
  187. package/src/server-fns/admin-permissions/list-registered.ts +5 -4
  188. package/src/server-fns/admin-permissions/set-role-abilities.ts +2 -0
  189. package/src/server-fns/admin-permissions/who-has.ts +2 -0
  190. package/src/server-fns/admin-roles/create.ts +2 -0
  191. package/src/server-fns/admin-roles/delete.ts +2 -0
  192. package/src/server-fns/admin-roles/get.ts +2 -0
  193. package/src/server-fns/admin-roles/list.ts +5 -4
  194. package/src/server-fns/admin-roles/reorder.ts +2 -0
  195. package/src/server-fns/admin-roles/update.ts +2 -0
  196. package/src/server-fns/admin-users/create.ts +2 -0
  197. package/src/server-fns/admin-users/delete.ts +2 -0
  198. package/src/server-fns/admin-users/disable.ts +2 -0
  199. package/src/server-fns/admin-users/enable.ts +2 -0
  200. package/src/server-fns/admin-users/get-user-roles.ts +2 -0
  201. package/src/server-fns/admin-users/get.ts +2 -0
  202. package/src/server-fns/admin-users/list.ts +2 -0
  203. package/src/server-fns/admin-users/set-password.ts +2 -0
  204. package/src/server-fns/admin-users/set-user-roles.ts +2 -0
  205. package/src/server-fns/admin-users/update.ts +2 -0
  206. package/src/server-fns/ai/execute.ts +3 -0
  207. package/src/server-fns/analytics/maintenance.ts +4 -0
  208. package/src/server-fns/analytics/queries.ts +14 -8
  209. package/src/server-fns/auth/current-user.ts +7 -3
  210. package/src/server-fns/auth/renew.test.node.ts +85 -0
  211. package/src/server-fns/auth/renew.ts +54 -0
  212. package/src/server-fns/auth/sign-in.test.node.ts +114 -0
  213. package/src/server-fns/auth/sign-in.ts +80 -27
  214. package/src/server-fns/auth/sign-out.test.node.ts +85 -0
  215. package/src/server-fns/auth/sign-out.ts +44 -19
  216. package/src/server-fns/collections/audit.ts +2 -0
  217. package/src/server-fns/collections/copy-to-locale.ts +3 -1
  218. package/src/server-fns/collections/create.ts +3 -1
  219. package/src/server-fns/collections/delete-locale.ts +3 -1
  220. package/src/server-fns/collections/delete.ts +3 -1
  221. package/src/server-fns/collections/duplicate.ts +3 -1
  222. package/src/server-fns/collections/get.ts +4 -1
  223. package/src/server-fns/collections/history.ts +2 -0
  224. package/src/server-fns/collections/list.ts +3 -1
  225. package/src/server-fns/collections/reindex.ts +2 -0
  226. package/src/server-fns/collections/reorder.ts +3 -1
  227. package/src/server-fns/collections/restore-version.ts +3 -1
  228. package/src/server-fns/collections/revision-input.test.node.ts +14 -0
  229. package/src/server-fns/collections/scheduled-publication.ts +14 -7
  230. package/src/server-fns/collections/set-list-view-preference.ts +2 -0
  231. package/src/server-fns/collections/stats.ts +2 -0
  232. package/src/server-fns/collections/status.ts +4 -1
  233. package/src/server-fns/collections/tree.ts +7 -1
  234. package/src/server-fns/collections/update.ts +4 -1
  235. package/src/server-fns/collections/upload-singleton.test.node.ts +8 -1
  236. package/src/server-fns/collections/upload.ts +10 -0
  237. package/src/server-fns/i18n/set-locale.ts +19 -3
  238. package/src/server-fns/preview/disable.ts +8 -4
  239. package/src/server-fns/preview/enable.ts +9 -5
  240. package/src/server-fns/singletons/cancel-scheduled-publish.ts +3 -1
  241. package/src/server-fns/singletons/change-status.ts +3 -1
  242. package/src/server-fns/singletons/confirm-scheduled-publish.ts +3 -1
  243. package/src/server-fns/singletons/copy-to-locale.ts +3 -1
  244. package/src/server-fns/singletons/find-by-version.ts +2 -0
  245. package/src/server-fns/singletons/get-scheduled-publish.ts +2 -0
  246. package/src/server-fns/singletons/get.ts +2 -0
  247. package/src/server-fns/singletons/history.ts +2 -0
  248. package/src/server-fns/singletons/restore-version.ts +3 -1
  249. package/src/server-fns/singletons/schedule-publish.ts +3 -1
  250. package/src/server-fns/singletons/singletons.test.node.ts +6 -0
  251. package/src/server-fns/singletons/unpublish.ts +3 -1
  252. package/src/server-fns/singletons/update.ts +3 -1
@@ -0,0 +1,114 @@
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
+ config: {} as any,
13
+ request: null as unknown as Request,
14
+ signIn: vi.fn(),
15
+ limiter: vi.fn(),
16
+ acquire: vi.fn(),
17
+ release: vi.fn(),
18
+ outcome: vi.fn(),
19
+ setCookies: vi.fn(),
20
+ }))
21
+ vi.mock('@tanstack/react-start', () => ({
22
+ createServerFn: () => ({
23
+ validator: (validate: (input: unknown) => unknown) => ({
24
+ handler: (handler: any) => ({ validate, handler }),
25
+ }),
26
+ }),
27
+ }))
28
+ vi.mock('@tanstack/react-start/server', () => ({
29
+ getRequest: () => mocks.request,
30
+ getRequestHeader: () => undefined,
31
+ }))
32
+ vi.mock('@byline/core', () => ({ getServerConfig: () => mocks.config }))
33
+ vi.mock('@byline/client/server', () => ({
34
+ setSessionCookies: mocks.setCookies,
35
+ readAccessTokenCookie: () => undefined,
36
+ readRefreshTokenCookie: () => undefined,
37
+ }))
38
+ vi.mock('../../i18n/locale-cookie.js', () => ({ readAdminLocaleCookie: () => null }))
39
+ vi.mock('../../integrations/byline-core.js', () => ({
40
+ bylineCore: () => {
41
+ throw new Error('unused')
42
+ },
43
+ }))
44
+
45
+ import { checkSignInBody } from '../../integrations/sign-in-body.js'
46
+ import { adminSignIn } from './sign-in.js'
47
+
48
+ const fn = adminSignIn as unknown as {
49
+ validate: (input: unknown) => unknown
50
+ handler: (args: any) => Promise<unknown>
51
+ }
52
+ const input = { email: 'admin@example.test', password: 'legacy' }
53
+ beforeEach(async () => {
54
+ vi.clearAllMocks()
55
+ mocks.request = new Request('http://localhost/sign-in', {
56
+ method: 'POST',
57
+ headers: { 'content-type': 'application/json' },
58
+ body: '{}',
59
+ })
60
+ await checkSignInBody(mocks.request)
61
+ mocks.acquire.mockResolvedValue(mocks.release)
62
+ mocks.limiter.mockResolvedValue({ allowed: true, retryAfterSeconds: 0 })
63
+ mocks.signIn.mockResolvedValue({ actor: { id: 'test' } })
64
+ mocks.config = {
65
+ sessionProvider: { signInWithPassword: mocks.signIn },
66
+ passwordSignIn: {
67
+ resolveClientIp: () => '192.0.2.1',
68
+ limiter: { consume: mocks.limiter, acquire: mocks.acquire, recordResult: mocks.outcome },
69
+ },
70
+ }
71
+ })
72
+ describe('password sign-in protection', () => {
73
+ it('admits validated legacy credentials and sets cookies after authentication', async () => {
74
+ expect(await fn.handler({ data: fn.validate(input) })).toEqual({ userId: 'test' })
75
+ expect(mocks.limiter).toHaveBeenCalledWith({ email: input.email, ip: '192.0.2.1' })
76
+ expect(mocks.signIn).toHaveBeenCalledWith({ ...input, ip: '192.0.2.1', userAgent: undefined })
77
+ expect(mocks.setCookies).toHaveBeenCalledTimes(1)
78
+ })
79
+ it('rejects oversized credentials during validation', () => {
80
+ expect(() => fn.validate({ ...input, password: 'a'.repeat(129) })).toThrow()
81
+ expect(mocks.signIn).not.toHaveBeenCalled()
82
+ })
83
+ it('returns 429 with Retry-After before the provider is called', async () => {
84
+ mocks.limiter.mockResolvedValue({ allowed: false, retryAfterSeconds: 42 })
85
+ const error = await fn.handler({ data: input }).catch((e) => e)
86
+ expect(error.status).toBe(429)
87
+ expect(error.headers.get('Retry-After')).toBe('42')
88
+ expect(mocks.signIn).not.toHaveBeenCalled()
89
+ expect(mocks.setCookies).not.toHaveBeenCalled()
90
+ })
91
+ it('sheds capacity before accessing the store or provider', async () => {
92
+ mocks.acquire.mockResolvedValue(null)
93
+ expect((await fn.handler({ data: input }).catch((e) => e)).status).toBe(429)
94
+ expect(mocks.limiter).not.toHaveBeenCalled()
95
+ expect(mocks.signIn).not.toHaveBeenCalled()
96
+ })
97
+ it('releases capacity and records failed verification', async () => {
98
+ mocks.signIn.mockRejectedValueOnce(new Error('invalid credentials'))
99
+ await expect(fn.handler({ data: input })).rejects.toThrow('invalid credentials')
100
+ expect(mocks.release).toHaveBeenCalledTimes(1)
101
+ expect(mocks.outcome).toHaveBeenCalledWith({ email: input.email, ip: '192.0.2.1' }, 'failure')
102
+ })
103
+ it.each(['store', 'ip', 'middleware', 'config'])(
104
+ 'fails closed when %s protection is unavailable',
105
+ async (kind) => {
106
+ if (kind === 'store') mocks.limiter.mockRejectedValue(new Error('DB unavailable'))
107
+ if (kind === 'ip') mocks.config.passwordSignIn.resolveClientIp = () => null
108
+ if (kind === 'middleware') mocks.request = new Request('http://localhost/sign-in')
109
+ if (kind === 'config') delete mocks.config.passwordSignIn
110
+ expect((await fn.handler({ data: input }).catch((e) => e)).status).toBe(503)
111
+ expect(mocks.signIn).not.toHaveBeenCalled()
112
+ }
113
+ )
114
+ })
@@ -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
@@ -1,4 +1,3 @@
1
- import { withDocumentMutationErrors } from '../document-mutation-errors.js'
2
1
  /**
3
2
  * This Source Code is subject to the terms of the Mozilla Public
4
3
  * License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -15,6 +14,8 @@ import type { CopyToLocaleResult, DocumentLifecycleContext } from '@byline/core/
15
14
  import { copyToLocale } from '@byline/core/services'
16
15
 
17
16
  import { ensureCollection } from '../../integrations/api-utils.js'
17
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
18
+ import { withDocumentMutationErrors } from '../document-mutation-errors.js'
18
19
  import {
19
20
  type CollectionDocumentCommittedHookFailureResponse,
20
21
  toCommittedDocumentHookFailureResponse,
@@ -34,6 +35,7 @@ import {
34
35
  * failures propagate to TanStack Start's transport layer.
35
36
  */
36
37
  export const copyDocumentToLocale = createServerFn({ method: 'POST' })
38
+ .middleware([adminSessionMiddleware])
37
39
  .validator(
38
40
  (input: {
39
41
  expectedRevision: number
@@ -1,4 +1,3 @@
1
- import { withDocumentMutationErrors } from '../document-mutation-errors.js'
2
1
  /**
3
2
  * This Source Code is subject to the terms of the Mozilla Public
4
3
  * License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -15,6 +14,8 @@ import type { CreateDocumentResult, DocumentLifecycleContext } from '@byline/cor
15
14
  import { createDocument } from '@byline/core/services'
16
15
 
17
16
  import { ensureCollection } from '../../integrations/api-utils.js'
17
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
18
+ import { withDocumentMutationErrors } from '../document-mutation-errors.js'
18
19
  import { toCommittedDocumentHookFailureResponse } from './save-outcome.js'
19
20
 
20
21
  // ---------------------------------------------------------------------------
@@ -22,6 +23,7 @@ import { toCommittedDocumentHookFailureResponse } from './save-outcome.js'
22
23
  // ---------------------------------------------------------------------------
23
24
 
24
25
  export const createCollectionDocument = createServerFn({ method: 'POST' })
26
+ .middleware([adminSessionMiddleware])
25
27
  .validator(
26
28
  (input: {
27
29
  collection: string
@@ -1,4 +1,3 @@
1
- import { withDocumentMutationErrors } from '../document-mutation-errors.js'
2
1
  /**
3
2
  * This Source Code is subject to the terms of the Mozilla Public
4
3
  * License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -15,6 +14,8 @@ import type { DeleteLocaleResult, DocumentLifecycleContext } from '@byline/core/
15
14
  import { deleteLocale } from '@byline/core/services'
16
15
 
17
16
  import { ensureCollection } from '../../integrations/api-utils.js'
17
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
18
+ import { withDocumentMutationErrors } from '../document-mutation-errors.js'
18
19
  import {
19
20
  type CollectionDocumentCommittedHookFailureResponse,
20
21
  toCommittedDocumentHookFailureResponse,
@@ -35,6 +36,7 @@ import {
35
36
  * transport layer.
36
37
  */
37
38
  export const deleteDocumentLocale = createServerFn({ method: 'POST' })
39
+ .middleware([adminSessionMiddleware])
38
40
  .validator(
39
41
  (input: { expectedRevision: number; collection: string; id: string; locale: string }) => input
40
42
  )
@@ -1,4 +1,3 @@
1
- import { withDocumentMutationErrors } from '../document-mutation-errors.js'
2
1
  /**
3
2
  * This Source Code is subject to the terms of the Mozilla Public
4
3
  * License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -15,6 +14,8 @@ import type { DocumentLifecycleContext } from '@byline/core/services'
15
14
  import { deleteDocument as deleteDocumentService } from '@byline/core/services'
16
15
 
17
16
  import { ensureCollection } from '../../integrations/api-utils.js'
17
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
18
+ import { withDocumentMutationErrors } from '../document-mutation-errors.js'
18
19
  import { toDeleteDocumentResponse } from './delete-outcome.js'
19
20
 
20
21
  // ---------------------------------------------------------------------------
@@ -22,6 +23,7 @@ import { toDeleteDocumentResponse } from './delete-outcome.js'
22
23
  // ---------------------------------------------------------------------------
23
24
 
24
25
  export const deleteDocument = createServerFn({ method: 'POST' })
26
+ .middleware([adminSessionMiddleware])
25
27
  .validator((input: { expectedRevision: number; collection: string; id: string }) => input)
26
28
  .handler(
27
29
  withDocumentMutationErrors(async ({ data: input }) => {
@@ -1,4 +1,3 @@
1
- import { withDocumentMutationErrors } from '../document-mutation-errors.js'
2
1
  /**
3
2
  * This Source Code is subject to the terms of the Mozilla Public
4
3
  * License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -15,6 +14,8 @@ import type { DocumentLifecycleContext, DuplicateDocumentResult } from '@byline/
15
14
  import { duplicateDocument } from '@byline/core/services'
16
15
 
17
16
  import { ensureCollection } from '../../integrations/api-utils.js'
17
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
18
+ import { withDocumentMutationErrors } from '../document-mutation-errors.js'
18
19
  import {
19
20
  type CollectionDocumentCommittedHookFailureResponse,
20
21
  toCommittedDocumentHookFailureResponse,
@@ -36,6 +37,7 @@ import {
36
37
  * TanStack Start's transport layer for the client to branch on.
37
38
  */
38
39
  export const duplicateCollectionDocument = createServerFn({ method: 'POST' })
40
+ .middleware([adminSessionMiddleware])
39
41
  .validator((input: { expectedRevision: number; collection: string; id: string }) => input)
40
42
  .handler(
41
43
  withDocumentMutationErrors(
@@ -1,4 +1,3 @@
1
- import { withEditableRevision } from '../editable-revision.js'
2
1
  /**
3
2
  * This Source Code is subject to the terms of the Mozilla Public
4
3
  * License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -21,7 +20,9 @@ import {
21
20
  } from '@byline/core'
22
21
 
23
22
  import { ensureCollection } from '../../integrations/api-utils.js'
23
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
24
24
  import { resolveAdminDocumentRead } from '../admin-document-presentation.js'
25
+ import { withEditableRevision } from '../editable-revision.js'
25
26
  import { serialise } from './utils'
26
27
 
27
28
  // ---------------------------------------------------------------------------
@@ -29,6 +30,7 @@ import { serialise } from './utils'
29
30
  // ---------------------------------------------------------------------------
30
31
 
31
32
  const getDocumentFn = createServerFn({ method: 'GET' })
33
+ .middleware([adminSessionMiddleware])
32
34
  .validator(
33
35
  (input: {
34
36
  collection: string
@@ -177,6 +179,7 @@ const getDocumentFn = createServerFn({ method: 'GET' })
177
179
  // ---------------------------------------------------------------------------
178
180
 
179
181
  const getDocumentByVersionFn = createServerFn({ method: 'GET' })
182
+ .middleware([adminSessionMiddleware])
180
183
  .validator((input: { collection: string; versionId: string; locale?: string }) => input)
181
184
  .handler(async ({ data }) => {
182
185
  const { collection: path, versionId, locale } = data
@@ -12,6 +12,7 @@ import { getAdminBylineClient } from '@byline/client/server'
12
12
  import { ERR_NOT_FOUND, getCollectionSchemasForPath, 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
  import { serialise } from './utils'
17
18
 
@@ -32,6 +33,7 @@ export interface HistorySearchParams {
32
33
  // ---------------------------------------------------------------------------
33
34
 
34
35
  export const getCollectionDocumentHistory = createServerFn({ method: 'GET' })
36
+ .middleware([adminSessionMiddleware])
35
37
  .validator((input: { collection: string; id: string; params: HistorySearchParams }) => input)
36
38
  .handler(async ({ data }) => {
37
39
  const { collection: path, id, params } = data
@@ -1,4 +1,3 @@
1
- import { withEditableRevision } from '../editable-revision.js'
2
1
  /**
3
2
  * This Source Code is subject to the terms of the Mozilla Public
4
3
  * License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -25,6 +24,8 @@ import {
25
24
 
26
25
  import { ensureCollection } from '../../integrations/api-utils.js'
27
26
  import { bylineCore } from '../../integrations/byline-core.js'
27
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
28
+ import { withEditableRevision } from '../editable-revision.js'
28
29
  import { resolveListViewState, sortableFieldNames } from './list-view-state.js'
29
30
  import { serialise } from './utils'
30
31
  import type { ListViewPreferenceValue } from './list-view-state.js'
@@ -49,6 +50,7 @@ export interface CollectionSearchParams {
49
50
  // ---------------------------------------------------------------------------
50
51
 
51
52
  export const getCollectionDocuments = createServerFn({ method: 'GET' })
53
+ .middleware([adminSessionMiddleware])
52
54
  .validator((input: { collection: string; params: CollectionSearchParams }) => input)
53
55
  .handler(async ({ data }) => {
54
56
  const { collection: path, params } = data
@@ -26,10 +26,12 @@ import { getAdminBylineClient } from '@byline/client/server'
26
26
  import { ERR_NOT_FOUND, getLogger } from '@byline/core'
27
27
 
28
28
  import { ensureCollection } from '../../integrations/api-utils.js'
29
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
29
30
 
30
31
  export type { ReindexResult }
31
32
 
32
33
  export const reindexCollection = createServerFn({ method: 'POST' })
34
+ .middleware([adminSessionMiddleware])
33
35
  .validator((input: { collection: string }) => input)
34
36
  .handler(async ({ data }): Promise<ReindexResult> => {
35
37
  const { collection } = data
@@ -1,4 +1,3 @@
1
- import { withDocumentMutationErrors } from '../document-mutation-errors.js'
2
1
  /**
3
2
  * This Source Code is subject to the terms of the Mozilla Public
4
3
  * License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -13,6 +12,8 @@ import { getAdminRequestContext } from '@byline/client/server'
13
12
  import { ERR_NOT_FOUND, getLogger, getServerConfig, reorderDocument } from '@byline/core'
14
13
 
15
14
  import { ensureCollection } from '../../integrations/api-utils.js'
15
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
16
+ import { withDocumentMutationErrors } from '../document-mutation-errors.js'
16
17
 
17
18
  // ---------------------------------------------------------------------------
18
19
  // Reorder a single document within an `orderable: true` collection.
@@ -30,6 +31,7 @@ import { ensureCollection } from '../../integrations/api-utils.js'
30
31
  // ---------------------------------------------------------------------------
31
32
 
32
33
  export const reorderCollectionDocument = createServerFn({ method: 'POST' })
34
+ .middleware([adminSessionMiddleware])
33
35
  .validator(
34
36
  (input: {
35
37
  expectedRevision: number