@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
@@ -12,12 +12,14 @@ import { deleteAdminRoleCommand } from '@byline/admin/admin-roles'
12
12
  import { getAdminRequestContext } from '@byline/client/server'
13
13
 
14
14
  import { bylineCore } from '../../integrations/byline-core.js'
15
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
15
16
 
16
17
  // Return shape mirrors `OkResponse` from `@byline/admin/admin-users`.
17
18
  // Lifting the type to a shared spot is a small follow-up — the two
18
19
  // modules each declare their own identical `OkResponse` today, and the
19
20
  // root `@byline/admin` barrel can only re-export one of them.
20
21
  export const deleteAdminRole = createServerFn({ method: 'POST' })
22
+ .middleware([adminSessionMiddleware])
21
23
  .validator((input: { id: string; vid: number }) => input)
22
24
  .handler(async ({ data }): Promise<{ ok: true }> => {
23
25
  const context = await getAdminRequestContext()
@@ -12,8 +12,10 @@ import { type AdminRoleResponse, getAdminRoleCommand } from '@byline/admin/admin
12
12
  import { getAdminRequestContext } from '@byline/client/server'
13
13
 
14
14
  import { bylineCore } from '../../integrations/byline-core.js'
15
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
15
16
 
16
17
  export const getAdminRole = createServerFn({ method: 'GET' })
18
+ .middleware([adminSessionMiddleware])
17
19
  .validator((input: { id: string }) => input)
18
20
  .handler(async ({ data }): Promise<AdminRoleResponse> => {
19
21
  const context = await getAdminRequestContext()
@@ -12,6 +12,7 @@ import { type AdminRoleListResponse, listAdminRolesCommand } from '@byline/admin
12
12
  import { getAdminRequestContext } from '@byline/client/server'
13
13
 
14
14
  import { bylineCore } from '../../integrations/byline-core.js'
15
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
15
16
 
16
17
  /**
17
18
  * List admin roles. Thin server-fn wrapper over `listAdminRolesCommand`
@@ -19,9 +20,9 @@ import { bylineCore } from '../../integrations/byline-core.js'
19
20
  * No paging or sorting at this layer; the role set is small by design
20
21
  * and ordered by the `order` column.
21
22
  */
22
- export const listAdminRoles = createServerFn({ method: 'GET' }).handler(
23
- async (): Promise<AdminRoleListResponse> => {
23
+ export const listAdminRoles = createServerFn({ method: 'GET' })
24
+ .middleware([adminSessionMiddleware])
25
+ .handler(async (): Promise<AdminRoleListResponse> => {
24
26
  const context = await getAdminRequestContext()
25
27
  return listAdminRolesCommand(context, {}, { store: bylineCore().adminStore! })
26
- }
27
- )
28
+ })
@@ -12,8 +12,10 @@ import { reorderAdminRolesCommand } from '@byline/admin/admin-roles'
12
12
  import { getAdminRequestContext } from '@byline/client/server'
13
13
 
14
14
  import { bylineCore } from '../../integrations/byline-core.js'
15
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
15
16
 
16
17
  export const reorderAdminRoles = createServerFn({ method: 'POST' })
18
+ .middleware([adminSessionMiddleware])
17
19
  .validator((input: { ids: string[] }) => input)
18
20
  .handler(async ({ data }): Promise<{ ok: true }> => {
19
21
  const context = await getAdminRequestContext()
@@ -12,6 +12,7 @@ import { type AdminRoleResponse, updateAdminRoleCommand } from '@byline/admin/ad
12
12
  import { getAdminRequestContext } from '@byline/client/server'
13
13
 
14
14
  import { bylineCore } from '../../integrations/byline-core.js'
15
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
15
16
 
16
17
  export interface UpdateAdminRoleInput {
17
18
  id: string
@@ -24,6 +25,7 @@ export interface UpdateAdminRoleInput {
24
25
  }
25
26
 
26
27
  export const updateAdminRole = createServerFn({ method: 'POST' })
28
+ .middleware([adminSessionMiddleware])
27
29
  .validator((input: UpdateAdminRoleInput) => input)
28
30
  .handler(async ({ data }): Promise<AdminRoleResponse> => {
29
31
  const context = await getAdminRequestContext()
@@ -12,6 +12,7 @@ import { type AdminUserResponse, createAdminUserCommand } from '@byline/admin/ad
12
12
  import { getAdminRequestContext } from '@byline/client/server'
13
13
 
14
14
  import { bylineCore } from '../../integrations/byline-core.js'
15
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
15
16
 
16
17
  export interface CreateAdminUserInput {
17
18
  email: string
@@ -25,6 +26,7 @@ export interface CreateAdminUserInput {
25
26
  }
26
27
 
27
28
  export const createAdminUser = createServerFn({ method: 'POST' })
29
+ .middleware([adminSessionMiddleware])
28
30
  .validator((input: CreateAdminUserInput) => input)
29
31
  .handler(async ({ data }): Promise<AdminUserResponse> => {
30
32
  const context = await getAdminRequestContext()
@@ -12,8 +12,10 @@ import { deleteAdminUserCommand, type OkResponse } from '@byline/admin/admin-use
12
12
  import { getAdminRequestContext } from '@byline/client/server'
13
13
 
14
14
  import { bylineCore } from '../../integrations/byline-core.js'
15
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
15
16
 
16
17
  export const deleteAdminUser = createServerFn({ method: 'POST' })
18
+ .middleware([adminSessionMiddleware])
17
19
  .validator((input: { id: string; vid: number }) => input)
18
20
  .handler(async ({ data }): Promise<OkResponse> => {
19
21
  const context = await getAdminRequestContext()
@@ -12,11 +12,13 @@ import { disableAdminUserCommand, type OkResponse } from '@byline/admin/admin-us
12
12
  import { getAdminRequestContext } from '@byline/client/server'
13
13
 
14
14
  import { bylineCore } from '../../integrations/byline-core.js'
15
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
15
16
 
16
17
  // No UI affordance yet — paired with `enable.ts`, both ship ahead of the
17
18
  // detail-view enable/disable control they will be wired into. Keep until
18
19
  // that control lands; not dead code.
19
20
  export const disableAdminUser = createServerFn({ method: 'POST' })
21
+ .middleware([adminSessionMiddleware])
20
22
  .validator((input: { id: string }) => input)
21
23
  .handler(async ({ data }): Promise<OkResponse> => {
22
24
  const context = await getAdminRequestContext()
@@ -12,11 +12,13 @@ import { enableAdminUserCommand, type OkResponse } from '@byline/admin/admin-use
12
12
  import { getAdminRequestContext } from '@byline/client/server'
13
13
 
14
14
  import { bylineCore } from '../../integrations/byline-core.js'
15
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
15
16
 
16
17
  // No UI affordance yet — paired with `disable.ts`, both ship ahead of the
17
18
  // detail-view enable/disable control they will be wired into. Keep until
18
19
  // that control lands; not dead code.
19
20
  export const enableAdminUser = createServerFn({ method: 'POST' })
21
+ .middleware([adminSessionMiddleware])
20
22
  .validator((input: { id: string }) => input)
21
23
  .handler(async ({ data }): Promise<OkResponse> => {
22
24
  const context = await getAdminRequestContext()
@@ -12,8 +12,10 @@ import { getRolesForUserCommand, type UserRolesResponse } from '@byline/admin/ad
12
12
  import { getAdminRequestContext } from '@byline/client/server'
13
13
 
14
14
  import { bylineCore } from '../../integrations/byline-core.js'
15
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
15
16
 
16
17
  export const getUserRoles = createServerFn({ method: 'GET' })
18
+ .middleware([adminSessionMiddleware])
17
19
  .validator((input: { userId: string }) => input)
18
20
  .handler(async ({ data }): Promise<UserRolesResponse> => {
19
21
  const context = await getAdminRequestContext()
@@ -12,8 +12,10 @@ import { type AdminUserResponse, getAdminUserCommand } from '@byline/admin/admin
12
12
  import { getAdminRequestContext } from '@byline/client/server'
13
13
 
14
14
  import { bylineCore } from '../../integrations/byline-core.js'
15
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
15
16
 
16
17
  export const getAdminUser = createServerFn({ method: 'GET' })
18
+ .middleware([adminSessionMiddleware])
17
19
  .validator((input: { id: string }) => input)
18
20
  .handler(async ({ data }): Promise<AdminUserResponse> => {
19
21
  const context = await getAdminRequestContext()
@@ -16,6 +16,7 @@ import {
16
16
  import { getAdminRequestContext } from '@byline/client/server'
17
17
 
18
18
  import { bylineCore } from '../../integrations/byline-core.js'
19
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
19
20
 
20
21
  export interface ListAdminUsersInput {
21
22
  page?: number
@@ -33,6 +34,7 @@ export interface ListAdminUsersInput {
33
34
  * TanStack Start transport layer for the client to branch on.
34
35
  */
35
36
  export const listAdminUsers = createServerFn({ method: 'GET' })
37
+ .middleware([adminSessionMiddleware])
36
38
  .validator((input: ListAdminUsersInput) => input ?? {})
37
39
  .handler(async ({ data }): Promise<AdminUserListResponse> => {
38
40
  const context = await getAdminRequestContext()
@@ -12,6 +12,7 @@ import { type AdminUserResponse, setAdminUserPasswordCommand } from '@byline/adm
12
12
  import { getAdminRequestContext } from '@byline/client/server'
13
13
 
14
14
  import { bylineCore } from '../../integrations/byline-core.js'
15
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
15
16
 
16
17
  export interface SetAdminUserPasswordInput {
17
18
  id: string
@@ -20,6 +21,7 @@ export interface SetAdminUserPasswordInput {
20
21
  }
21
22
 
22
23
  export const setAdminUserPassword = createServerFn({ method: 'POST' })
24
+ .middleware([adminSessionMiddleware])
23
25
  .validator((input: SetAdminUserPasswordInput) => input)
24
26
  .handler(async ({ data }): Promise<AdminUserResponse> => {
25
27
  const context = await getAdminRequestContext()
@@ -12,6 +12,7 @@ import { setRolesForUserCommand, type UserRolesResponse } from '@byline/admin/ad
12
12
  import { getAdminRequestContext } from '@byline/client/server'
13
13
 
14
14
  import { bylineCore } from '../../integrations/byline-core.js'
15
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
15
16
 
16
17
  export interface SetUserRolesInput {
17
18
  userId: string
@@ -19,6 +20,7 @@ export interface SetUserRolesInput {
19
20
  }
20
21
 
21
22
  export const setUserRoles = createServerFn({ method: 'POST' })
23
+ .middleware([adminSessionMiddleware])
22
24
  .validator((input: SetUserRolesInput) => input)
23
25
  .handler(async ({ data }): Promise<UserRolesResponse> => {
24
26
  const context = await getAdminRequestContext()
@@ -12,6 +12,7 @@ import { type AdminUserResponse, updateAdminUserCommand } from '@byline/admin/ad
12
12
  import { getAdminRequestContext } from '@byline/client/server'
13
13
 
14
14
  import { bylineCore } from '../../integrations/byline-core.js'
15
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
15
16
 
16
17
  export interface UpdateAdminUserInput {
17
18
  id: string
@@ -27,6 +28,7 @@ export interface UpdateAdminUserInput {
27
28
  }
28
29
 
29
30
  export const updateAdminUser = createServerFn({ method: 'POST' })
31
+ .middleware([adminSessionMiddleware])
30
32
  .validator((input: UpdateAdminUserInput) => input)
31
33
  .handler(async ({ data }): Promise<AdminUserResponse> => {
32
34
  const context = await getAdminRequestContext()
@@ -26,6 +26,8 @@ import type { ExecuteInstructionOptions, ExecuteInstructionParams } from '@bylin
26
26
  import { executeInstruction, executeInstructionStreaming } from '@byline/ai/server'
27
27
  import { getAdminRequestContext } from '@byline/client/server'
28
28
 
29
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
30
+
29
31
  /**
30
32
  * Wire shape — the same as `ExecuteInstruction` minus the in-process-only
31
33
  * `signal` (an AbortSignal can't be serialized across the RPC boundary;
@@ -37,6 +39,7 @@ type ExecuteAiInstructionInput = {
37
39
  }
38
40
 
39
41
  export const executeAiInstruction = createServerFn({ method: 'POST' })
42
+ .middleware([adminSessionMiddleware])
40
43
  .validator((input: ExecuteAiInstructionInput) => input)
41
44
  .handler(async ({ data }): Promise<Response> => {
42
45
  // Throws ERR_UNAUTHENTICATED if there is no admin session.
@@ -12,6 +12,8 @@ import { assertAdminActor } from '@byline/admin'
12
12
  import { ANALYTICS_ABILITIES } from '@byline/admin/analytics'
13
13
  import { getAdminRequestContext } from '@byline/client/server'
14
14
 
15
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
16
+
15
17
  export interface DeleteAnalyticsEventsInput {
16
18
  from: string
17
19
  to: string
@@ -19,6 +21,7 @@ export interface DeleteAnalyticsEventsInput {
19
21
  }
20
22
 
21
23
  export const deleteAnalyticsEvents = createServerFn({ method: 'POST' })
24
+ .middleware([adminSessionMiddleware])
22
25
  .validator((input: DeleteAnalyticsEventsInput) => input)
23
26
  .handler(async ({ data }): Promise<{ deleted: number }> => {
24
27
  await assertAnalyticsMaintenance()
@@ -34,6 +37,7 @@ export const deleteAnalyticsEvents = createServerFn({ method: 'POST' })
34
37
  })
35
38
 
36
39
  export const rebuildAnalyticsDay = createServerFn({ method: 'POST' })
40
+ .middleware([adminSessionMiddleware])
37
41
  .validator((input: { day: string }) => input)
38
42
  .handler(async ({ data }): Promise<{ rebuilt: string }> => {
39
43
  await assertAnalyticsMaintenance()
@@ -22,6 +22,8 @@ import type {
22
22
  } from '@byline/analytics'
23
23
  import { getAdminRequestContext } from '@byline/client/server'
24
24
 
25
+ import { adminSessionMiddleware } from '../../integrations/session-middleware.js'
26
+
25
27
  export interface AnalyticsTopInput extends AnalyticsDateRange {
26
28
  kind: AnalyticsEventKind
27
29
  limit?: number
@@ -31,16 +33,17 @@ export interface AnalyticsRuntimeState {
31
33
  enabled: boolean
32
34
  }
33
35
 
34
- export const getAnalyticsRuntime = createServerFn({ method: 'GET' }).handler(
35
- async (): Promise<AnalyticsRuntimeState> => {
36
+ export const getAnalyticsRuntime = createServerFn({ method: 'GET' })
37
+ .middleware([adminSessionMiddleware])
38
+ .handler(async (): Promise<AnalyticsRuntimeState> => {
36
39
  const context = await getAdminRequestContext()
37
40
  requireAdminActor(context, 'reading analytics runtime state')
38
41
  const { isAnalyticsRegistered } = await import('@byline/analytics')
39
42
  return { enabled: isAnalyticsRegistered() }
40
- }
41
- )
43
+ })
42
44
 
43
45
  export const getAnalyticsSummary = createServerFn({ method: 'GET' })
46
+ .middleware([adminSessionMiddleware])
44
47
  .validator((input: AnalyticsDateRange) => input)
45
48
  .handler(async ({ data }): Promise<AnalyticsSummary> => {
46
49
  await assertAnalyticsRead()
@@ -48,15 +51,16 @@ export const getAnalyticsSummary = createServerFn({ method: 'GET' })
48
51
  return getAnalytics().getSummary(data)
49
52
  })
50
53
 
51
- export const getAnalyticsReportCoverage = createServerFn({ method: 'GET' }).handler(
52
- async (): Promise<AnalyticsReportCoverage> => {
54
+ export const getAnalyticsReportCoverage = createServerFn({ method: 'GET' })
55
+ .middleware([adminSessionMiddleware])
56
+ .handler(async (): Promise<AnalyticsReportCoverage> => {
53
57
  await assertAnalyticsRead()
54
58
  const { getAnalytics } = await import('@byline/analytics')
55
59
  return getAnalytics().getReportCoverage()
56
- }
57
- )
60
+ })
58
61
 
59
62
  export const getAnalyticsTop = createServerFn({ method: 'GET' })
63
+ .middleware([adminSessionMiddleware])
60
64
  .validator((input: AnalyticsTopInput) => input)
61
65
  .handler(async ({ data }): Promise<AnalyticsRankedTotals<AnalyticsPathTotal>> => {
62
66
  await assertAnalyticsRead()
@@ -65,6 +69,7 @@ export const getAnalyticsTop = createServerFn({ method: 'GET' })
65
69
  })
66
70
 
67
71
  export const getAnalyticsReferrers = createServerFn({ method: 'GET' })
72
+ .middleware([adminSessionMiddleware])
68
73
  .validator((input: AnalyticsDateRange & { limit?: number }) => input)
69
74
  .handler(async ({ data }): Promise<AnalyticsRankedTotals<AnalyticsReferrerTotal>> => {
70
75
  await assertAnalyticsRead()
@@ -73,6 +78,7 @@ export const getAnalyticsReferrers = createServerFn({ method: 'GET' })
73
78
  })
74
79
 
75
80
  export const getAnalyticsCountries = createServerFn({ method: 'GET' })
81
+ .middleware([adminSessionMiddleware])
76
82
  .validator((input: AnalyticsDateRange) => input)
77
83
  .handler(async ({ data }): Promise<AnalyticsCountryTotal[]> => {
78
84
  await assertAnalyticsRead()
@@ -10,7 +10,7 @@
10
10
  * Admin profile lookup for the authenticated request.
11
11
  *
12
12
  * Thin wrapper that resolves the `AdminAuth` via `getAdminRequestContext()`
13
- * (which handles verify + lazy refresh + cookie rewrites), then fetches the
13
+ * (which verifies credentials without rotating or rewriting cookies), then fetches the
14
14
  * admin user's profile row so the UI can render "signed in as X" without
15
15
  * embedding identity metadata in the JWT itself.
16
16
  *
@@ -26,6 +26,7 @@ import { getAdminRequestContext } from '@byline/client/server'
26
26
  import { bylineCore } from '../../integrations/byline-core.js'
27
27
 
28
28
  export interface CurrentAdminUser {
29
+ sessionId: string
29
30
  id: string
30
31
  email: string
31
32
  given_name: string | null
@@ -48,7 +49,7 @@ export interface CurrentAdminUser {
48
49
 
49
50
  export const getCurrentAdminUser = createServerFn({ method: 'GET' }).handler(
50
51
  async (): Promise<CurrentAdminUser> => {
51
- const { actor } = await getAdminRequestContext()
52
+ const { actor, sessionId } = await getAdminRequestContext()
52
53
  if (!actor) {
53
54
  // getAdminRequestContext always returns an AdminAuth on success; the
54
55
  // null branch is compile-time impossible here but satisfies the type.
@@ -63,6 +64,7 @@ export const getCurrentAdminUser = createServerFn({ method: 'GET' }).handler(
63
64
  }
64
65
 
65
66
  return {
67
+ sessionId: sessionId!,
66
68
  id: row.id,
67
69
  email: row.email,
68
70
  given_name: row.given_name,
@@ -85,8 +87,9 @@ export const getCurrentAdminUserSoft = createServerFn({ method: 'GET' }).handler
85
87
  async (): Promise<CurrentAdminUser | null> => {
86
88
  try {
87
89
  let actor: Actor
90
+ let sessionId: string | undefined
88
91
  try {
89
- ;({ actor } = await getAdminRequestContext())
92
+ ;({ actor, sessionId } = await getAdminRequestContext())
90
93
  } catch (err) {
91
94
  if (err instanceof AuthError) return null
92
95
  throw err
@@ -97,6 +100,7 @@ export const getCurrentAdminUserSoft = createServerFn({ method: 'GET' }).handler
97
100
  if (!row) return null
98
101
 
99
102
  return {
103
+ sessionId: sessionId!,
100
104
  id: row.id,
101
105
  email: row.email,
102
106
  given_name: row.given_name,
@@ -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 { ERR_ACCESS_EXPIRED, ERR_INVALID_TOKEN, ERR_REVOKED_TOKEN } from '@byline/auth'
10
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
11
+
12
+ const mocks = vi.hoisted(() => ({
13
+ access: 'access' as string | undefined,
14
+ refresh: 'refresh' as string | undefined,
15
+ verify: vi.fn(),
16
+ rotate: vi.fn(),
17
+ cookies: vi.fn(),
18
+ }))
19
+ vi.mock('@tanstack/react-start', () => ({
20
+ createServerFn: () => ({
21
+ validator: (validate: any) => ({ handler: (handler: any) => ({ validate, handler }) }),
22
+ }),
23
+ }))
24
+ vi.mock('@tanstack/react-start/server', () => ({ setResponseHeader: vi.fn() }))
25
+ vi.mock('@byline/core', () => ({
26
+ getServerConfig: () => ({
27
+ sessionProvider: { verifyAccessToken: mocks.verify, refreshSession: mocks.rotate },
28
+ }),
29
+ }))
30
+ vi.mock('@byline/client/server', () => ({
31
+ readAccessTokenCookie: () => mocks.access,
32
+ readRefreshTokenCookie: () => mocks.refresh,
33
+ setSessionCookies: mocks.cookies,
34
+ }))
35
+
36
+ import { renewAdminSession } from './renew.js'
37
+
38
+ const endpoint = renewAdminSession as unknown as { handler: (args: any) => Promise<any> }
39
+ const call = () => endpoint.handler({ data: { expectedSessionId: 'A' } })
40
+ beforeEach(() => {
41
+ vi.resetAllMocks()
42
+ mocks.access = 'access'
43
+ mocks.refresh = 'refresh'
44
+ })
45
+ describe('explicit renewal', () => {
46
+ it('does not rotate or write cookies when access is already valid', async () => {
47
+ mocks.verify.mockResolvedValue({ sessionId: 'A' })
48
+ await expect(call()).resolves.toEqual({ sessionId: 'A' })
49
+ expect(mocks.rotate).not.toHaveBeenCalled()
50
+ expect(mocks.cookies).not.toHaveBeenCalled()
51
+ })
52
+ it('passes the original login identity into renewal and writes only confirmed credentials', async () => {
53
+ mocks.verify.mockRejectedValue(ERR_ACCESS_EXPIRED({ message: 'expired' }))
54
+ const tokens = { sessionId: 'A', accessToken: 'new', refreshToken: 'new-refresh' }
55
+ mocks.rotate.mockImplementation(async () => {
56
+ expect(mocks.cookies).not.toHaveBeenCalled()
57
+ return tokens
58
+ })
59
+ await expect(call()).resolves.toEqual({ sessionId: 'A' })
60
+ expect(mocks.rotate).toHaveBeenCalledWith({ refreshToken: 'refresh', expectedSessionId: 'A' })
61
+ expect(mocks.cookies).toHaveBeenCalledWith(tokens)
62
+ })
63
+ it.each([
64
+ ERR_INVALID_TOKEN({ message: 'bad signature' }),
65
+ ERR_REVOKED_TOKEN({ message: 'revoked' }),
66
+ new Error('database'),
67
+ ])('does not turn %s into renewal', async (error) => {
68
+ mocks.verify.mockRejectedValue(error)
69
+ await expect(call()).rejects.toBe(error)
70
+ expect(mocks.rotate).not.toHaveBeenCalled()
71
+ expect(mocks.cookies).not.toHaveBeenCalled()
72
+ })
73
+ it('rejects a different live login instead of accepting the no-op', async () => {
74
+ mocks.verify.mockResolvedValue({ sessionId: 'B' })
75
+ await expect(call()).rejects.toMatchObject({ code: 'ERR_SESSION_CHANGED' })
76
+ expect(mocks.rotate).not.toHaveBeenCalled()
77
+ expect(mocks.cookies).not.toHaveBeenCalled()
78
+ })
79
+ it('preserves the cookie jar on renewal failure', async () => {
80
+ mocks.access = undefined
81
+ mocks.rotate.mockRejectedValue(ERR_REVOKED_TOKEN({ message: 'replayed' }))
82
+ await expect(call()).rejects.toThrow()
83
+ expect(mocks.cookies).not.toHaveBeenCalled()
84
+ })
85
+ })
@@ -0,0 +1,54 @@
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 { createServerFn } from '@tanstack/react-start'
10
+ import { setResponseHeader } from '@tanstack/react-start/server'
11
+
12
+ import { AuthError, ERR_SESSION_CHANGED, ERR_UNAUTHENTICATED } from '@byline/auth'
13
+ import {
14
+ readAccessTokenCookie,
15
+ readRefreshTokenCookie,
16
+ setSessionCookies,
17
+ } from '@byline/client/server'
18
+ import { getServerConfig } from '@byline/core'
19
+
20
+ /** Explicit renewal only. Global CSRF protection applies before this POST handler. */
21
+ export const renewAdminSession = createServerFn({ method: 'POST' })
22
+ .validator((value: unknown): { expectedSessionId?: string } => {
23
+ if (value == null) return {}
24
+ if (typeof value !== 'object') throw new Error('Invalid renewal input')
25
+ const sid = (value as { expectedSessionId?: unknown }).expectedSessionId
26
+ if (sid !== undefined && (typeof sid !== 'string' || !sid || sid.length > 256))
27
+ throw new Error('Invalid login identity')
28
+ return { expectedSessionId: sid as string | undefined }
29
+ })
30
+ .handler(async ({ data }) => {
31
+ setResponseHeader('Cache-Control', 'no-store')
32
+ const provider = getServerConfig().sessionProvider
33
+ if (!provider) throw new Error('no sessionProvider configured')
34
+ const access = readAccessTokenCookie()
35
+ if (access) {
36
+ try {
37
+ const verified = await provider.verifyAccessToken(access)
38
+ if (data.expectedSessionId && data.expectedSessionId !== verified.sessionId)
39
+ throw ERR_SESSION_CHANGED({ message: 'session changed' })
40
+ return { sessionId: verified.sessionId }
41
+ } catch (error) {
42
+ // Invalid signature, revocation and infrastructure errors never cause renewal.
43
+ if (!(error instanceof AuthError) || error.code !== 'ERR_ACCESS_EXPIRED') throw error
44
+ }
45
+ }
46
+ const refreshToken = readRefreshTokenCookie()
47
+ if (!refreshToken) throw ERR_UNAUTHENTICATED({ message: 'no session to renew' })
48
+ const tokens = await provider.refreshSession({
49
+ refreshToken,
50
+ expectedSessionId: data.expectedSessionId,
51
+ })
52
+ setSessionCookies(tokens)
53
+ return { sessionId: tokens.sessionId }
54
+ })
@@ -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
+ })