@byline/host-tanstack-start 5.0.0 → 5.1.1

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 (258) 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/optional-i18n-provider.d.ts +29 -0
  4. package/dist/admin-shell/chrome/optional-i18n-provider.js +24 -0
  5. package/dist/admin-shell/chrome/session-change-boundary.d.ts +14 -0
  6. package/dist/admin-shell/chrome/session-change-boundary.js +119 -0
  7. package/dist/admin-shell/chrome/session-change-boundary.module.js +11 -0
  8. package/dist/admin-shell/chrome/session-change-boundary.test.d.ts +8 -0
  9. package/dist/admin-shell/chrome/session-change-boundary_module.css +81 -0
  10. package/dist/admin-shell/chrome/sign-in-page.d.ts +2 -1
  11. package/dist/admin-shell/chrome/sign-in-page.js +60 -3
  12. package/dist/integrations/byline-admin-services.js +7 -1
  13. package/dist/integrations/client-ip.d.ts +19 -0
  14. package/dist/integrations/client-ip.js +34 -0
  15. package/dist/integrations/session-coordination.d.ts +24 -0
  16. package/dist/integrations/session-coordination.js +85 -0
  17. package/dist/integrations/session-middleware.d.ts +11 -0
  18. package/dist/integrations/session-middleware.js +94 -0
  19. package/dist/integrations/session-request-kind.d.ts +9 -0
  20. package/dist/integrations/session-request-kind.js +10 -0
  21. package/dist/integrations/session-request-middleware.d.ts +9 -0
  22. package/dist/integrations/session-request-middleware.js +7 -0
  23. package/dist/integrations/sign-in-body.d.ts +12 -0
  24. package/dist/integrations/sign-in-body.js +52 -0
  25. package/dist/integrations/sign-in-middleware.d.ts +9 -0
  26. package/dist/integrations/sign-in-middleware.js +10 -0
  27. package/dist/routes/create-admin-layout-route.js +46 -31
  28. package/dist/routes/create-sign-in-route.js +4 -2
  29. package/dist/server-fns/admin-account/change-password.d.ts +3 -1
  30. package/dist/server-fns/admin-account/change-password.js +9 -3
  31. package/dist/server-fns/admin-account/get.d.ts +3 -1
  32. package/dist/server-fns/admin-account/get.js +4 -1
  33. package/dist/server-fns/admin-account/index.d.ts +5 -5
  34. package/dist/server-fns/admin-account/update.d.ts +3 -1
  35. package/dist/server-fns/admin-account/update.js +4 -1
  36. package/dist/server-fns/admin-activity/get.d.ts +3 -1
  37. package/dist/server-fns/admin-activity/get.js +4 -1
  38. package/dist/server-fns/admin-permissions/get-role-abilities.d.ts +3 -1
  39. package/dist/server-fns/admin-permissions/get-role-abilities.js +4 -1
  40. package/dist/server-fns/admin-permissions/index.d.ts +4 -4
  41. package/dist/server-fns/admin-permissions/list-registered.d.ts +3 -1
  42. package/dist/server-fns/admin-permissions/list-registered.js +4 -1
  43. package/dist/server-fns/admin-permissions/set-role-abilities.d.ts +3 -1
  44. package/dist/server-fns/admin-permissions/set-role-abilities.js +4 -1
  45. package/dist/server-fns/admin-permissions/who-has.d.ts +3 -1
  46. package/dist/server-fns/admin-permissions/who-has.js +4 -1
  47. package/dist/server-fns/admin-roles/create.d.ts +3 -1
  48. package/dist/server-fns/admin-roles/create.js +4 -1
  49. package/dist/server-fns/admin-roles/delete.d.ts +3 -1
  50. package/dist/server-fns/admin-roles/delete.js +4 -1
  51. package/dist/server-fns/admin-roles/get.d.ts +3 -1
  52. package/dist/server-fns/admin-roles/get.js +4 -1
  53. package/dist/server-fns/admin-roles/index.d.ts +6 -6
  54. package/dist/server-fns/admin-roles/list.d.ts +3 -1
  55. package/dist/server-fns/admin-roles/list.js +4 -1
  56. package/dist/server-fns/admin-roles/reorder.d.ts +3 -1
  57. package/dist/server-fns/admin-roles/reorder.js +4 -1
  58. package/dist/server-fns/admin-roles/update.d.ts +3 -1
  59. package/dist/server-fns/admin-roles/update.js +4 -1
  60. package/dist/server-fns/admin-users/create.d.ts +3 -1
  61. package/dist/server-fns/admin-users/create.js +4 -1
  62. package/dist/server-fns/admin-users/delete.d.ts +3 -1
  63. package/dist/server-fns/admin-users/delete.js +4 -1
  64. package/dist/server-fns/admin-users/disable.d.ts +3 -1
  65. package/dist/server-fns/admin-users/disable.js +4 -1
  66. package/dist/server-fns/admin-users/enable.d.ts +3 -1
  67. package/dist/server-fns/admin-users/enable.js +4 -1
  68. package/dist/server-fns/admin-users/get-user-roles.d.ts +3 -1
  69. package/dist/server-fns/admin-users/get-user-roles.js +4 -1
  70. package/dist/server-fns/admin-users/get.d.ts +3 -1
  71. package/dist/server-fns/admin-users/get.js +4 -1
  72. package/dist/server-fns/admin-users/index.d.ts +10 -10
  73. package/dist/server-fns/admin-users/list.d.ts +3 -1
  74. package/dist/server-fns/admin-users/list.js +4 -1
  75. package/dist/server-fns/admin-users/set-password.d.ts +3 -1
  76. package/dist/server-fns/admin-users/set-password.js +4 -1
  77. package/dist/server-fns/admin-users/set-user-roles.d.ts +3 -1
  78. package/dist/server-fns/admin-users/set-user-roles.js +4 -1
  79. package/dist/server-fns/admin-users/update.d.ts +3 -1
  80. package/dist/server-fns/admin-users/update.js +4 -1
  81. package/dist/server-fns/ai/execute.d.ts +3 -1
  82. package/dist/server-fns/ai/execute.js +4 -1
  83. package/dist/server-fns/ai/index.d.ts +1 -1
  84. package/dist/server-fns/analytics/maintenance.d.ts +6 -2
  85. package/dist/server-fns/analytics/maintenance.js +7 -2
  86. package/dist/server-fns/analytics/queries.d.ts +18 -6
  87. package/dist/server-fns/analytics/queries.js +19 -6
  88. package/dist/server-fns/auth/current-user.d.ts +1 -0
  89. package/dist/server-fns/auth/current-user.js +5 -2
  90. package/dist/server-fns/auth/renew.d.ts +13 -0
  91. package/dist/server-fns/auth/renew.js +45 -0
  92. package/dist/server-fns/auth/sign-in.d.ts +2 -1
  93. package/dist/server-fns/auth/sign-in.js +88 -17
  94. package/dist/server-fns/auth/sign-out.d.ts +3 -1
  95. package/dist/server-fns/auth/sign-out.js +38 -6
  96. package/dist/server-fns/collections/audit.d.ts +3 -1
  97. package/dist/server-fns/collections/audit.js +4 -1
  98. package/dist/server-fns/collections/copy-to-locale.d.ts +10 -1
  99. package/dist/server-fns/collections/copy-to-locale.js +5 -2
  100. package/dist/server-fns/collections/create.d.ts +10 -1
  101. package/dist/server-fns/collections/create.js +5 -2
  102. package/dist/server-fns/collections/delete-locale.d.ts +10 -1
  103. package/dist/server-fns/collections/delete-locale.js +5 -2
  104. package/dist/server-fns/collections/delete.d.ts +10 -1
  105. package/dist/server-fns/collections/delete.js +5 -2
  106. package/dist/server-fns/collections/duplicate.d.ts +10 -1
  107. package/dist/server-fns/collections/duplicate.js +5 -2
  108. package/dist/server-fns/collections/get.d.ts +7 -0
  109. package/dist/server-fns/collections/get.js +8 -3
  110. package/dist/server-fns/collections/history.d.ts +3 -1
  111. package/dist/server-fns/collections/history.js +4 -1
  112. package/dist/server-fns/collections/index.d.ts +21 -21
  113. package/dist/server-fns/collections/list.d.ts +10 -1
  114. package/dist/server-fns/collections/list.js +5 -2
  115. package/dist/server-fns/collections/reindex.d.ts +3 -1
  116. package/dist/server-fns/collections/reindex.js +4 -1
  117. package/dist/server-fns/collections/reorder.d.ts +10 -1
  118. package/dist/server-fns/collections/reorder.js +5 -2
  119. package/dist/server-fns/collections/restore-version.d.ts +10 -1
  120. package/dist/server-fns/collections/restore-version.js +5 -2
  121. package/dist/server-fns/collections/scheduled-publication.d.ts +22 -5
  122. package/dist/server-fns/collections/scheduled-publication.js +17 -6
  123. package/dist/server-fns/collections/set-list-view-preference.d.ts +3 -1
  124. package/dist/server-fns/collections/set-list-view-preference.js +4 -1
  125. package/dist/server-fns/collections/stats.js +4 -1
  126. package/dist/server-fns/collections/status.d.ts +13 -2
  127. package/dist/server-fns/collections/status.js +8 -3
  128. package/dist/server-fns/collections/tree.d.ts +22 -5
  129. package/dist/server-fns/collections/tree.js +17 -6
  130. package/dist/server-fns/collections/update.d.ts +13 -2
  131. package/dist/server-fns/collections/update.js +8 -3
  132. package/dist/server-fns/collections/upload.d.ts +10 -1
  133. package/dist/server-fns/collections/upload.js +4 -1
  134. package/dist/server-fns/i18n/set-locale.d.ts +3 -1
  135. package/dist/server-fns/i18n/set-locale.js +18 -3
  136. package/dist/server-fns/preview/disable.d.ts +3 -1
  137. package/dist/server-fns/preview/disable.js +4 -1
  138. package/dist/server-fns/preview/enable.d.ts +3 -1
  139. package/dist/server-fns/preview/enable.js +4 -1
  140. package/dist/server-fns/singletons/cancel-scheduled-publish.d.ts +10 -1
  141. package/dist/server-fns/singletons/cancel-scheduled-publish.js +5 -2
  142. package/dist/server-fns/singletons/change-status.d.ts +10 -1
  143. package/dist/server-fns/singletons/change-status.js +5 -2
  144. package/dist/server-fns/singletons/confirm-scheduled-publish.d.ts +10 -1
  145. package/dist/server-fns/singletons/confirm-scheduled-publish.js +5 -2
  146. package/dist/server-fns/singletons/copy-to-locale.d.ts +10 -1
  147. package/dist/server-fns/singletons/copy-to-locale.js +5 -2
  148. package/dist/server-fns/singletons/find-by-version.d.ts +3 -1
  149. package/dist/server-fns/singletons/find-by-version.js +4 -1
  150. package/dist/server-fns/singletons/get-scheduled-publish.d.ts +3 -1
  151. package/dist/server-fns/singletons/get-scheduled-publish.js +4 -1
  152. package/dist/server-fns/singletons/get.d.ts +3 -1
  153. package/dist/server-fns/singletons/get.js +4 -1
  154. package/dist/server-fns/singletons/history.d.ts +3 -1
  155. package/dist/server-fns/singletons/history.js +4 -1
  156. package/dist/server-fns/singletons/restore-version.d.ts +10 -1
  157. package/dist/server-fns/singletons/restore-version.js +5 -2
  158. package/dist/server-fns/singletons/schedule-publish.d.ts +10 -1
  159. package/dist/server-fns/singletons/schedule-publish.js +5 -2
  160. package/dist/server-fns/singletons/unpublish.d.ts +10 -1
  161. package/dist/server-fns/singletons/unpublish.js +5 -2
  162. package/dist/server-fns/singletons/update.d.ts +10 -1
  163. package/dist/server-fns/singletons/update.js +5 -2
  164. package/package.json +19 -19
  165. package/src/admin-shell/chrome/admin-app-bar.tsx +11 -6
  166. package/src/admin-shell/chrome/optional-i18n-provider.tsx +61 -0
  167. package/src/admin-shell/chrome/session-change-boundary.module.css +112 -0
  168. package/src/admin-shell/chrome/session-change-boundary.test.tsx +91 -0
  169. package/src/admin-shell/chrome/session-change-boundary.tsx +155 -0
  170. package/src/admin-shell/chrome/sign-in-page.tsx +68 -3
  171. package/src/integrations/byline-admin-services.ts +7 -1
  172. package/src/integrations/client-ip.test.node.ts +45 -0
  173. package/src/integrations/client-ip.ts +54 -0
  174. package/src/integrations/framework-fetch-shape.test.node.ts +56 -0
  175. package/src/integrations/session-client.test.node.ts +121 -0
  176. package/src/integrations/session-coordination.test.node.ts +77 -0
  177. package/src/integrations/session-coordination.ts +96 -0
  178. package/src/integrations/session-middleware.test.node.ts +87 -0
  179. package/src/integrations/session-middleware.ts +107 -0
  180. package/src/integrations/session-request-kind.ts +18 -0
  181. package/src/integrations/session-request-middleware.ts +17 -0
  182. package/src/integrations/sign-in-body.test.node.ts +59 -0
  183. package/src/integrations/sign-in-body.ts +50 -0
  184. package/src/integrations/sign-in-middleware.ts +24 -0
  185. package/src/routes/create-admin-layout-route.tsx +48 -30
  186. package/src/routes/create-sign-in-route.tsx +10 -2
  187. package/src/server-fns/admin-account/change-password.test.node.ts +69 -0
  188. package/src/server-fns/admin-account/change-password.ts +13 -2
  189. package/src/server-fns/admin-account/get.ts +2 -0
  190. package/src/server-fns/admin-account/update.ts +2 -0
  191. package/src/server-fns/admin-activity/get.ts +2 -0
  192. package/src/server-fns/admin-permissions/get-role-abilities.ts +2 -0
  193. package/src/server-fns/admin-permissions/list-registered.ts +5 -4
  194. package/src/server-fns/admin-permissions/set-role-abilities.ts +2 -0
  195. package/src/server-fns/admin-permissions/who-has.ts +2 -0
  196. package/src/server-fns/admin-roles/create.ts +2 -0
  197. package/src/server-fns/admin-roles/delete.ts +2 -0
  198. package/src/server-fns/admin-roles/get.ts +2 -0
  199. package/src/server-fns/admin-roles/list.ts +5 -4
  200. package/src/server-fns/admin-roles/reorder.ts +2 -0
  201. package/src/server-fns/admin-roles/update.ts +2 -0
  202. package/src/server-fns/admin-users/create.ts +2 -0
  203. package/src/server-fns/admin-users/delete.ts +2 -0
  204. package/src/server-fns/admin-users/disable.ts +2 -0
  205. package/src/server-fns/admin-users/enable.ts +2 -0
  206. package/src/server-fns/admin-users/get-user-roles.ts +2 -0
  207. package/src/server-fns/admin-users/get.ts +2 -0
  208. package/src/server-fns/admin-users/list.ts +2 -0
  209. package/src/server-fns/admin-users/set-password.ts +2 -0
  210. package/src/server-fns/admin-users/set-user-roles.ts +2 -0
  211. package/src/server-fns/admin-users/update.ts +2 -0
  212. package/src/server-fns/ai/execute.ts +3 -0
  213. package/src/server-fns/analytics/maintenance.ts +4 -0
  214. package/src/server-fns/analytics/queries.ts +14 -8
  215. package/src/server-fns/auth/current-user.ts +7 -3
  216. package/src/server-fns/auth/renew.test.node.ts +85 -0
  217. package/src/server-fns/auth/renew.ts +54 -0
  218. package/src/server-fns/auth/sign-in.test.node.ts +114 -0
  219. package/src/server-fns/auth/sign-in.ts +80 -27
  220. package/src/server-fns/auth/sign-out.test.node.ts +85 -0
  221. package/src/server-fns/auth/sign-out.ts +44 -19
  222. package/src/server-fns/collections/audit.ts +2 -0
  223. package/src/server-fns/collections/copy-to-locale.ts +3 -1
  224. package/src/server-fns/collections/create.ts +3 -1
  225. package/src/server-fns/collections/delete-locale.ts +3 -1
  226. package/src/server-fns/collections/delete.ts +3 -1
  227. package/src/server-fns/collections/duplicate.ts +3 -1
  228. package/src/server-fns/collections/get.ts +4 -1
  229. package/src/server-fns/collections/history.ts +2 -0
  230. package/src/server-fns/collections/list.ts +3 -1
  231. package/src/server-fns/collections/reindex.ts +2 -0
  232. package/src/server-fns/collections/reorder.ts +3 -1
  233. package/src/server-fns/collections/restore-version.ts +3 -1
  234. package/src/server-fns/collections/revision-input.test.node.ts +14 -0
  235. package/src/server-fns/collections/scheduled-publication.ts +14 -7
  236. package/src/server-fns/collections/set-list-view-preference.ts +2 -0
  237. package/src/server-fns/collections/stats.ts +2 -0
  238. package/src/server-fns/collections/status.ts +4 -1
  239. package/src/server-fns/collections/tree.ts +7 -1
  240. package/src/server-fns/collections/update.ts +4 -1
  241. package/src/server-fns/collections/upload-singleton.test.node.ts +8 -1
  242. package/src/server-fns/collections/upload.ts +10 -0
  243. package/src/server-fns/i18n/set-locale.ts +19 -3
  244. package/src/server-fns/preview/disable.ts +8 -4
  245. package/src/server-fns/preview/enable.ts +9 -5
  246. package/src/server-fns/singletons/cancel-scheduled-publish.ts +3 -1
  247. package/src/server-fns/singletons/change-status.ts +3 -1
  248. package/src/server-fns/singletons/confirm-scheduled-publish.ts +3 -1
  249. package/src/server-fns/singletons/copy-to-locale.ts +3 -1
  250. package/src/server-fns/singletons/find-by-version.ts +2 -0
  251. package/src/server-fns/singletons/get-scheduled-publish.ts +2 -0
  252. package/src/server-fns/singletons/get.ts +2 -0
  253. package/src/server-fns/singletons/history.ts +2 -0
  254. package/src/server-fns/singletons/restore-version.ts +3 -1
  255. package/src/server-fns/singletons/schedule-publish.ts +3 -1
  256. package/src/server-fns/singletons/singletons.test.node.ts +6 -0
  257. package/src/server-fns/singletons/unpublish.ts +3 -1
  258. package/src/server-fns/singletons/update.ts +3 -1
@@ -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
+ })
@@ -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
  /**