@byline/host-tanstack-start 3.21.0 → 4.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 (236) hide show
  1. package/dist/admin-shell/admin-activity/list.js +15 -5
  2. package/dist/admin-shell/admin-roles/delete.js +2 -1
  3. package/dist/admin-shell/admin-roles/list.js +2 -1
  4. package/dist/admin-shell/admin-users/delete.js +2 -1
  5. package/dist/admin-shell/admin-users/list.js +4 -7
  6. package/dist/admin-shell/chrome/admin-app-bar.js +2 -1
  7. package/dist/admin-shell/chrome/admin-menu-drawer.js +9 -8
  8. package/dist/admin-shell/chrome/admin-menu-provider.js +2 -1
  9. package/dist/admin-shell/chrome/dashboard.js +4 -3
  10. package/dist/admin-shell/chrome/sign-in-page.d.ts +2 -2
  11. package/dist/admin-shell/chrome/sign-in-page.js +2 -2
  12. package/dist/admin-shell/collections/create.js +4 -3
  13. package/dist/admin-shell/collections/document-history.js +6 -1
  14. package/dist/admin-shell/collections/edit.js +18 -14
  15. package/dist/admin-shell/collections/history.js +5 -4
  16. package/dist/admin-shell/collections/list.js +8 -7
  17. package/dist/admin-shell/collections/restore-version-modal.js +2 -1
  18. package/dist/admin-shell/collections/tree-list.js +53 -17
  19. package/dist/admin-shell/collections/view-menu.js +9 -8
  20. package/dist/i18n/resolve-locale.js +1 -1
  21. package/dist/index.d.ts +5 -6
  22. package/dist/integrations/abilities.js +8 -7
  23. package/dist/integrations/host-bridge.d.ts +13 -0
  24. package/dist/integrations/host-bridge.js +22 -0
  25. package/dist/integrations/serve-uploads.d.ts +1 -0
  26. package/dist/integrations/serve-uploads.js +1 -0
  27. package/dist/integrations/start-errors.js +2 -1
  28. package/dist/routes/admin-path.d.ts +22 -0
  29. package/dist/routes/admin-path.js +35 -0
  30. package/dist/routes/create-admin-account-route.js +3 -2
  31. package/dist/routes/create-admin-activity-route.js +3 -2
  32. package/dist/routes/create-admin-dashboard-route.js +2 -1
  33. package/dist/routes/create-admin-layout-route.d.ts +1 -1
  34. package/dist/routes/create-admin-layout-route.js +3 -1
  35. package/dist/routes/create-admin-permissions-route.js +3 -2
  36. package/dist/routes/create-admin-role-edit-route.js +4 -3
  37. package/dist/routes/create-admin-roles-list-route.js +3 -2
  38. package/dist/routes/create-admin-user-edit-route.js +4 -3
  39. package/dist/routes/create-admin-users-list-route.js +3 -2
  40. package/dist/routes/create-collection-api-route.js +5 -4
  41. package/dist/routes/create-collection-create-route.js +4 -3
  42. package/dist/routes/create-collection-edit-route.js +4 -3
  43. package/dist/routes/create-collection-history-route.js +5 -4
  44. package/dist/routes/create-collection-list-route.js +3 -2
  45. package/dist/routes/create-sign-in-route.js +3 -2
  46. package/dist/routes/index.d.ts +3 -0
  47. package/dist/routes/index.js +2 -0
  48. package/dist/routes/sign-in-path.d.ts +13 -0
  49. package/dist/routes/sign-in-path.js +19 -0
  50. package/dist/server-fns/admin-account/change-password.js +1 -1
  51. package/dist/server-fns/admin-account/get.js +1 -1
  52. package/dist/server-fns/admin-account/update.js +1 -1
  53. package/dist/server-fns/admin-activity/get.js +1 -1
  54. package/dist/server-fns/admin-permissions/get-role-abilities.js +1 -1
  55. package/dist/server-fns/admin-permissions/list-registered.js +1 -1
  56. package/dist/server-fns/admin-permissions/set-role-abilities.js +1 -1
  57. package/dist/server-fns/admin-permissions/who-has.js +1 -1
  58. package/dist/server-fns/admin-roles/create.js +1 -1
  59. package/dist/server-fns/admin-roles/delete.js +1 -1
  60. package/dist/server-fns/admin-roles/get.js +1 -1
  61. package/dist/server-fns/admin-roles/list.js +1 -1
  62. package/dist/server-fns/admin-roles/reorder.js +1 -1
  63. package/dist/server-fns/admin-roles/update.js +1 -1
  64. package/dist/server-fns/admin-users/create.js +1 -1
  65. package/dist/server-fns/admin-users/delete.js +1 -1
  66. package/dist/server-fns/admin-users/disable.js +1 -1
  67. package/dist/server-fns/admin-users/enable.js +1 -1
  68. package/dist/server-fns/admin-users/get-user-roles.js +1 -1
  69. package/dist/server-fns/admin-users/get.js +1 -1
  70. package/dist/server-fns/admin-users/list.js +1 -1
  71. package/dist/server-fns/admin-users/set-password.js +1 -1
  72. package/dist/server-fns/admin-users/set-user-roles.js +1 -1
  73. package/dist/server-fns/admin-users/update.js +1 -1
  74. package/dist/server-fns/ai/execute.js +1 -1
  75. package/dist/server-fns/auth/current-user.js +1 -1
  76. package/dist/server-fns/auth/sign-in.js +1 -1
  77. package/dist/server-fns/auth/sign-out.js +1 -2
  78. package/dist/server-fns/collections/audit.js +1 -1
  79. package/dist/server-fns/collections/copy-to-locale.js +1 -1
  80. package/dist/server-fns/collections/create.js +1 -1
  81. package/dist/server-fns/collections/delete-locale.js +1 -1
  82. package/dist/server-fns/collections/delete-outcome.d.ts +30 -0
  83. package/dist/server-fns/collections/delete-outcome.js +34 -0
  84. package/dist/server-fns/collections/delete.d.ts +1 -4
  85. package/dist/server-fns/collections/delete.js +3 -5
  86. package/dist/server-fns/collections/duplicate.js +1 -1
  87. package/dist/server-fns/collections/get.js +1 -1
  88. package/dist/server-fns/collections/history.js +1 -1
  89. package/dist/server-fns/collections/index.d.ts +1 -0
  90. package/dist/server-fns/collections/index.js +1 -0
  91. package/dist/server-fns/collections/list.js +1 -1
  92. package/dist/server-fns/collections/reindex.js +1 -1
  93. package/dist/server-fns/collections/reorder.js +1 -1
  94. package/dist/server-fns/collections/restore-version.js +1 -1
  95. package/dist/server-fns/collections/stats.js +1 -1
  96. package/dist/server-fns/collections/status.js +1 -1
  97. package/dist/server-fns/collections/tree-mutation.d.ts +46 -0
  98. package/dist/server-fns/collections/tree-mutation.js +109 -0
  99. package/dist/server-fns/collections/tree-read.d.ts +6 -0
  100. package/dist/server-fns/collections/tree-read.js +15 -0
  101. package/dist/server-fns/collections/tree.d.ts +5 -4
  102. package/dist/server-fns/collections/tree.js +16 -13
  103. package/dist/server-fns/collections/update.js +3 -2
  104. package/dist/server-fns/collections/upload.js +1 -1
  105. package/dist/server-fns/i18n/set-locale.js +1 -1
  106. package/dist/server-fns/preview/disable.js +1 -1
  107. package/dist/server-fns/preview/enable.js +1 -2
  108. package/dist/server-fns/preview/state.js +1 -1
  109. package/dist/vite/client-hook-boundary.d.ts +19 -0
  110. package/dist/vite/client-hook-boundary.js +20 -0
  111. package/dist/{integrations/byline-public-client.d.ts → vite/index.d.ts} +1 -2
  112. package/dist/vite/index.js +1 -0
  113. package/package.json +15 -14
  114. package/src/admin-shell/admin-activity/list.tsx +13 -3
  115. package/src/admin-shell/admin-roles/delete.tsx +2 -1
  116. package/src/admin-shell/admin-roles/list.tsx +2 -1
  117. package/src/admin-shell/admin-users/delete.tsx +3 -2
  118. package/src/admin-shell/admin-users/list.tsx +9 -8
  119. package/src/admin-shell/chrome/admin-app-bar.tsx +2 -1
  120. package/src/admin-shell/chrome/admin-menu-drawer.tsx +13 -8
  121. package/src/admin-shell/chrome/admin-menu-provider.tsx +2 -1
  122. package/src/admin-shell/chrome/dashboard.tsx +4 -3
  123. package/src/admin-shell/chrome/sign-in-page.tsx +3 -3
  124. package/src/admin-shell/collections/create.tsx +4 -3
  125. package/src/admin-shell/collections/document-history.tsx +5 -0
  126. package/src/admin-shell/collections/edit.tsx +20 -13
  127. package/src/admin-shell/collections/history.tsx +6 -5
  128. package/src/admin-shell/collections/list.tsx +8 -7
  129. package/src/admin-shell/collections/restore-version-modal.tsx +2 -1
  130. package/src/admin-shell/collections/tree-list.tsx +78 -22
  131. package/src/admin-shell/collections/view-menu.tsx +9 -8
  132. package/src/i18n/resolve-locale.ts +1 -1
  133. package/src/index.ts +5 -6
  134. package/src/integrations/abilities.tsx +14 -4
  135. package/src/integrations/host-bridge.ts +56 -0
  136. package/src/integrations/serve-uploads.ts +6 -0
  137. package/src/integrations/start-errors.test.node.ts +22 -0
  138. package/src/integrations/start-errors.ts +1 -0
  139. package/src/routes/admin-path.test.node.ts +129 -0
  140. package/src/routes/admin-path.ts +66 -0
  141. package/src/routes/auth-routes.test.node.ts +39 -0
  142. package/src/routes/create-admin-account-route.tsx +3 -2
  143. package/src/routes/create-admin-activity-route.tsx +3 -2
  144. package/src/routes/create-admin-dashboard-route.tsx +2 -1
  145. package/src/routes/create-admin-layout-route.tsx +4 -4
  146. package/src/routes/create-admin-permissions-route.tsx +3 -2
  147. package/src/routes/create-admin-role-edit-route.tsx +7 -3
  148. package/src/routes/create-admin-roles-list-route.tsx +3 -2
  149. package/src/routes/create-admin-user-edit-route.tsx +7 -3
  150. package/src/routes/create-admin-users-list-route.tsx +3 -2
  151. package/src/routes/create-collection-api-route.tsx +8 -4
  152. package/src/routes/create-collection-create-route.tsx +7 -3
  153. package/src/routes/create-collection-edit-route.tsx +7 -3
  154. package/src/routes/create-collection-history-route.tsx +8 -4
  155. package/src/routes/create-collection-list-route.tsx +3 -2
  156. package/src/routes/create-sign-in-route.tsx +9 -3
  157. package/src/routes/index.ts +11 -0
  158. package/src/routes/sign-in-path.test.node.ts +79 -0
  159. package/src/routes/sign-in-path.ts +36 -0
  160. package/src/server-fns/admin-account/change-password.ts +1 -1
  161. package/src/server-fns/admin-account/get.ts +1 -1
  162. package/src/server-fns/admin-account/update.ts +1 -1
  163. package/src/server-fns/admin-activity/get.ts +1 -1
  164. package/src/server-fns/admin-permissions/get-role-abilities.ts +1 -1
  165. package/src/server-fns/admin-permissions/list-registered.ts +1 -1
  166. package/src/server-fns/admin-permissions/set-role-abilities.ts +1 -1
  167. package/src/server-fns/admin-permissions/who-has.ts +1 -1
  168. package/src/server-fns/admin-roles/create.ts +1 -1
  169. package/src/server-fns/admin-roles/delete.ts +1 -1
  170. package/src/server-fns/admin-roles/get.ts +1 -1
  171. package/src/server-fns/admin-roles/list.ts +1 -1
  172. package/src/server-fns/admin-roles/reorder.ts +1 -1
  173. package/src/server-fns/admin-roles/update.ts +1 -1
  174. package/src/server-fns/admin-users/create.ts +1 -1
  175. package/src/server-fns/admin-users/delete.ts +1 -1
  176. package/src/server-fns/admin-users/disable.ts +1 -1
  177. package/src/server-fns/admin-users/enable.ts +1 -1
  178. package/src/server-fns/admin-users/get-user-roles.ts +1 -1
  179. package/src/server-fns/admin-users/get.ts +1 -1
  180. package/src/server-fns/admin-users/list.ts +1 -1
  181. package/src/server-fns/admin-users/set-password.ts +1 -1
  182. package/src/server-fns/admin-users/set-user-roles.ts +1 -1
  183. package/src/server-fns/admin-users/update.ts +1 -1
  184. package/src/server-fns/ai/execute.ts +1 -2
  185. package/src/server-fns/auth/current-user.ts +1 -1
  186. package/src/server-fns/auth/sign-in.ts +1 -1
  187. package/src/server-fns/auth/sign-out.ts +5 -3
  188. package/src/server-fns/collections/audit.ts +1 -1
  189. package/src/server-fns/collections/copy-to-locale.ts +1 -1
  190. package/src/server-fns/collections/create.ts +1 -1
  191. package/src/server-fns/collections/delete-locale.ts +1 -1
  192. package/src/server-fns/collections/delete-outcome.test.node.ts +72 -0
  193. package/src/server-fns/collections/delete-outcome.ts +80 -0
  194. package/src/server-fns/collections/delete.ts +3 -2
  195. package/src/server-fns/collections/duplicate.ts +1 -1
  196. package/src/server-fns/collections/get.ts +3 -1
  197. package/src/server-fns/collections/history.ts +1 -1
  198. package/src/server-fns/collections/index.ts +1 -0
  199. package/src/server-fns/collections/list.ts +1 -1
  200. package/src/server-fns/collections/reindex.ts +1 -1
  201. package/src/server-fns/collections/reorder.ts +1 -1
  202. package/src/server-fns/collections/restore-version.ts +1 -1
  203. package/src/server-fns/collections/stats.ts +2 -1
  204. package/src/server-fns/collections/status.ts +1 -1
  205. package/src/server-fns/collections/tree-mutation.test.node.ts +225 -0
  206. package/src/server-fns/collections/tree-mutation.ts +165 -0
  207. package/src/server-fns/collections/tree-read.test.node.ts +46 -0
  208. package/src/server-fns/collections/tree-read.ts +26 -0
  209. package/src/server-fns/collections/tree.ts +27 -11
  210. package/src/server-fns/collections/update.ts +4 -1
  211. package/src/server-fns/collections/upload.ts +1 -1
  212. package/src/server-fns/i18n/set-locale.ts +1 -1
  213. package/src/server-fns/preview/disable.ts +1 -1
  214. package/src/server-fns/preview/enable.ts +1 -2
  215. package/src/server-fns/preview/state.ts +1 -1
  216. package/src/vite/client-hook-boundary.test.node.ts +50 -0
  217. package/src/vite/client-hook-boundary.ts +45 -0
  218. package/src/vite/index.ts +9 -0
  219. package/dist/auth/auth-context.d.ts +0 -37
  220. package/dist/auth/auth-context.js +0 -57
  221. package/dist/auth/auth-cookies.d.ts +0 -34
  222. package/dist/auth/auth-cookies.js +0 -46
  223. package/dist/auth/preview-cookies.d.ts +0 -19
  224. package/dist/auth/preview-cookies.js +0 -26
  225. package/dist/integrations/byline-client.d.ts +0 -24
  226. package/dist/integrations/byline-client.js +0 -25
  227. package/dist/integrations/byline-public-client.js +0 -15
  228. package/dist/integrations/byline-viewer-client.d.ts +0 -20
  229. package/dist/integrations/byline-viewer-client.js +0 -39
  230. package/src/auth/auth-context.test.node.ts +0 -215
  231. package/src/auth/auth-context.ts +0 -124
  232. package/src/auth/auth-cookies.ts +0 -113
  233. package/src/auth/preview-cookies.ts +0 -73
  234. package/src/integrations/byline-client.ts +0 -66
  235. package/src/integrations/byline-public-client.ts +0 -53
  236. package/src/integrations/byline-viewer-client.ts +0 -99
@@ -1,215 +0,0 @@
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
- /**
10
- * Unit coverage for the admin request-context resolver. Mocks the
11
- * TanStack Start cookie helpers and the `bylineCore.sessionProvider` so
12
- * the branches can be exercised without a live Postgres or JWT
13
- * machinery.
14
- */
15
-
16
- import { AdminAuth, AuthError, AuthErrorCodes } from '@byline/auth'
17
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
18
-
19
- // ---------------------------------------------------------------------------
20
- // Mocks — must be declared before the module under test is imported.
21
- // `vi.mock` is hoisted to the top of the file, so the fakes themselves
22
- // have to go through `vi.hoisted` to be available when the factory runs.
23
- // ---------------------------------------------------------------------------
24
-
25
- const mocks = vi.hoisted(() => ({
26
- verifyAccessToken: vi.fn(),
27
- refreshSession: vi.fn(),
28
- getCookie: vi.fn(),
29
- setCookie: vi.fn(),
30
- getRequestHeader: vi.fn(),
31
- }))
32
-
33
- vi.mock('@byline/core', async () => {
34
- const actual = await vi.importActual<typeof import('@byline/core')>('@byline/core')
35
- return {
36
- ...actual,
37
- getServerConfig: () => ({
38
- sessionProvider: {
39
- verifyAccessToken: mocks.verifyAccessToken,
40
- refreshSession: mocks.refreshSession,
41
- },
42
- }),
43
- }
44
- })
45
-
46
- vi.mock('@tanstack/react-start/server', () => ({
47
- getCookie: mocks.getCookie,
48
- setCookie: mocks.setCookie,
49
- getRequestHeader: mocks.getRequestHeader,
50
- }))
51
-
52
- const { verifyAccessToken, refreshSession, getCookie, setCookie } = mocks
53
-
54
- // Import AFTER mocks are declared.
55
- import { getAdminRequestContext } from './auth-context.js'
56
-
57
- // ---------------------------------------------------------------------------
58
-
59
- function cookiesReturn(values: Record<string, string | undefined>) {
60
- getCookie.mockImplementation((name: string) => values[name])
61
- }
62
-
63
- function stubActor() {
64
- return new AdminAuth({
65
- id: 'admin-1',
66
- abilities: ['collections.pages.read'],
67
- })
68
- }
69
-
70
- describe('getAdminRequestContext', () => {
71
- beforeEach(() => {
72
- vi.clearAllMocks()
73
- })
74
-
75
- afterEach(() => {
76
- vi.clearAllMocks()
77
- })
78
-
79
- it('returns the actor directly when the access token verifies', async () => {
80
- cookiesReturn({
81
- byline_access_token: 'valid-access',
82
- byline_refresh_token: 'some-refresh',
83
- })
84
- const actor = stubActor()
85
- verifyAccessToken.mockResolvedValueOnce({ actor })
86
-
87
- const ctx = await getAdminRequestContext()
88
-
89
- expect(ctx.actor).toBe(actor)
90
- expect(ctx.readMode).toBe('any')
91
- expect(refreshSession).not.toHaveBeenCalled()
92
- expect(setCookie).not.toHaveBeenCalled()
93
- })
94
-
95
- it('refreshes when the access token fails verification', async () => {
96
- cookiesReturn({
97
- byline_access_token: 'stale-access',
98
- byline_refresh_token: 'valid-refresh',
99
- })
100
- const actor = stubActor()
101
- verifyAccessToken.mockRejectedValueOnce(new Error('expired')).mockResolvedValueOnce({ actor })
102
- refreshSession.mockResolvedValueOnce({
103
- accessToken: 'fresh-access',
104
- refreshToken: 'fresh-refresh',
105
- accessTokenExpiresAt: new Date(Date.now() + 15 * 60_000),
106
- refreshTokenExpiresAt: new Date(Date.now() + 30 * 24 * 60 * 60_000),
107
- })
108
-
109
- const ctx = await getAdminRequestContext()
110
-
111
- expect(ctx.actor).toBe(actor)
112
- expect(refreshSession).toHaveBeenCalledWith({ refreshToken: 'valid-refresh' })
113
- // Both access + refresh cookies should have been written with maxAge > 0.
114
- expect(setCookie).toHaveBeenCalledTimes(2)
115
- const accessCall = setCookie.mock.calls.find((c) => c[0] === 'byline_access_token')
116
- expect(accessCall?.[1]).toBe('fresh-access')
117
- expect(accessCall?.[2]?.maxAge).toBeGreaterThan(0)
118
- })
119
-
120
- it('refreshes when the access cookie is missing but a refresh cookie exists', async () => {
121
- cookiesReturn({ byline_refresh_token: 'only-refresh' })
122
- const actor = stubActor()
123
- refreshSession.mockResolvedValueOnce({
124
- accessToken: 'fresh-access',
125
- refreshToken: 'fresh-refresh',
126
- accessTokenExpiresAt: new Date(Date.now() + 15 * 60_000),
127
- refreshTokenExpiresAt: new Date(Date.now() + 30 * 24 * 60 * 60_000),
128
- })
129
- verifyAccessToken.mockResolvedValueOnce({ actor })
130
-
131
- const ctx = await getAdminRequestContext()
132
-
133
- expect(ctx.actor).toBe(actor)
134
- // The access-token path was skipped (no cookie) so verifyAccessToken ran
135
- // only for the freshly-issued token.
136
- expect(verifyAccessToken).toHaveBeenCalledTimes(1)
137
- expect(verifyAccessToken).toHaveBeenCalledWith('fresh-access')
138
- })
139
-
140
- it('throws ERR_UNAUTHENTICATED without emitting Set-Cookie when no cookies are sent', async () => {
141
- cookiesReturn({})
142
-
143
- try {
144
- await getAdminRequestContext()
145
- expect.fail('expected ERR_UNAUTHENTICATED')
146
- } catch (err) {
147
- expect(err).toBeInstanceOf(AuthError)
148
- expect((err as AuthError).code).toBe(AuthErrorCodes.UNAUTHENTICATED)
149
- }
150
- // Anonymous visitors must produce zero Set-Cookie headers so shared
151
- // caches (Cloudflare) can cache public pages — a Set-Cookie on the
152
- // response is a hard bypass signal for CDNs.
153
- expect(setCookie).not.toHaveBeenCalled()
154
- })
155
-
156
- it('clears the stale access cookie when only an access token was sent', async () => {
157
- cookiesReturn({ byline_access_token: 'stale' })
158
- verifyAccessToken.mockRejectedValueOnce(new Error('expired'))
159
-
160
- try {
161
- await getAdminRequestContext()
162
- expect.fail('expected ERR_UNAUTHENTICATED')
163
- } catch (err) {
164
- expect((err as AuthError).code).toBe(AuthErrorCodes.UNAUTHENTICATED)
165
- }
166
- const clears = setCookie.mock.calls.filter((c) => c[2]?.maxAge === 0)
167
- const clearedNames = new Set(clears.map((c) => c[0]))
168
- expect(clearedNames.has('byline_access_token')).toBe(true)
169
- expect(clearedNames.has('byline_refresh_token')).toBe(true)
170
- })
171
-
172
- it('clears cookies and throws when refresh itself fails', async () => {
173
- cookiesReturn({
174
- byline_access_token: 'stale',
175
- byline_refresh_token: 'bad-refresh',
176
- })
177
- verifyAccessToken.mockRejectedValueOnce(new Error('expired'))
178
- refreshSession.mockRejectedValueOnce(new Error('revoked'))
179
-
180
- try {
181
- await getAdminRequestContext()
182
- expect.fail('expected ERR_UNAUTHENTICATED')
183
- } catch (err) {
184
- expect(err).toBeInstanceOf(AuthError)
185
- expect((err as AuthError).code).toBe(AuthErrorCodes.UNAUTHENTICATED)
186
- }
187
- const clears = setCookie.mock.calls.filter((c) => c[2]?.maxAge === 0)
188
- expect(clears.length).toBe(2)
189
- })
190
-
191
- it('clears cookies and throws when the refreshed access token fails to verify', async () => {
192
- cookiesReturn({
193
- byline_access_token: 'stale',
194
- byline_refresh_token: 'valid',
195
- })
196
- verifyAccessToken
197
- .mockRejectedValueOnce(new Error('expired'))
198
- .mockRejectedValueOnce(new Error('still bad'))
199
- refreshSession.mockResolvedValueOnce({
200
- accessToken: 'fresh-but-somehow-bad',
201
- refreshToken: 'fresh-refresh',
202
- accessTokenExpiresAt: new Date(Date.now() + 15 * 60_000),
203
- refreshTokenExpiresAt: new Date(Date.now() + 30 * 24 * 60 * 60_000),
204
- })
205
-
206
- try {
207
- await getAdminRequestContext()
208
- expect.fail('expected ERR_UNAUTHENTICATED')
209
- } catch (err) {
210
- expect((err as AuthError).code).toBe(AuthErrorCodes.UNAUTHENTICATED)
211
- }
212
- const clears = setCookie.mock.calls.filter((c) => c[2]?.maxAge === 0)
213
- expect(clears.length).toBe(2)
214
- })
215
- })
@@ -1,124 +0,0 @@
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
- /**
10
- * Request-scoped auth context for admin server functions.
11
- *
12
- * Reads the session cookies, verifies the access token, transparently
13
- * refreshes when needed, and returns a `RequestContext` carrying the
14
- * authenticated `AdminAuth`. Every admin server fn calls this as its
15
- * first step — it is the single point where the admin transport boundary
16
- * meets the actor/ability machinery.
17
- *
18
- * Flow, in order:
19
- *
20
- * 1. Read the access cookie.
21
- * 2. If present, try `sessionProvider.verifyAccessToken`. On success,
22
- * return the context — no DB write, no cookie churn.
23
- * 3. On verify failure (or missing access cookie), read the refresh
24
- * cookie. If present, call `sessionProvider.refreshSession` — this
25
- * rotates the refresh token atomically and issues a new access
26
- * token. Write both fresh cookies to the response.
27
- * 4. Verify the new access token (populate the actor) and return the
28
- * context.
29
- * 5. Any failure along the way clears both cookies so the browser
30
- * stops sending a session the server has already rejected, and
31
- * throws `ERR_UNAUTHENTICATED`.
32
- *
33
- * Burns a refresh-token rotation only when the access token actually
34
- * fails verification — not on every request. The "one session" UX is
35
- * the consequence of this helper working invisibly behind each call.
36
- */
37
-
38
- import { ERR_UNAUTHENTICATED, type RequestContext } from '@byline/auth'
39
- import { getServerConfig } from '@byline/core'
40
- import { v7 as uuidv7 } from 'uuid'
41
-
42
- import {
43
- clearSessionCookies,
44
- readAccessTokenCookie,
45
- readRefreshTokenCookie,
46
- setSessionCookies,
47
- } from './auth-cookies.js'
48
-
49
- function requireSessionProvider() {
50
- const provider = getServerConfig().sessionProvider
51
- if (!provider) {
52
- throw new Error(
53
- 'No sessionProvider configured on ServerConfig. ' +
54
- 'Construct a JwtSessionProvider in your server config and pass it to initBylineCore().'
55
- )
56
- }
57
- return provider
58
- }
59
-
60
- export async function getAdminRequestContext(): Promise<RequestContext> {
61
- const provider = requireSessionProvider()
62
-
63
- const accessToken = readAccessTokenCookie()
64
-
65
- // Happy path: valid access token.
66
- if (accessToken) {
67
- try {
68
- const { actor } = await provider.verifyAccessToken(accessToken)
69
- return {
70
- actor,
71
- requestId: uuidv7(),
72
- readMode: 'any',
73
- }
74
- } catch {
75
- // Fall through to refresh — we'll burn a rotation only when the
76
- // access token genuinely can't verify.
77
- }
78
- }
79
-
80
- // Refresh path: swap the refresh cookie for a fresh token pair.
81
- const refreshToken = readRefreshTokenCookie()
82
- if (!refreshToken) {
83
- // Only emit cookie clears when the browser actually sent a stale access
84
- // cookie — otherwise the response carries no Set-Cookie at all, which
85
- // lets shared caches (Cloudflare) cache public pages for anonymous
86
- // visitors. Set-Cookie on the response is a hard bypass signal for CDNs.
87
- if (accessToken) {
88
- clearSessionCookies()
89
- }
90
- throw ERR_UNAUTHENTICATED({ message: 'no admin session' })
91
- }
92
-
93
- let refreshed: Awaited<ReturnType<typeof provider.refreshSession>>
94
- try {
95
- refreshed = await provider.refreshSession({ refreshToken })
96
- } catch (err) {
97
- clearSessionCookies()
98
- throw ERR_UNAUTHENTICATED({
99
- message: 'admin session could not be refreshed',
100
- cause: err,
101
- })
102
- }
103
-
104
- // Write the new cookies so subsequent requests skip the refresh path.
105
- setSessionCookies(refreshed)
106
-
107
- // Verify the freshly-minted access token to extract the actor.
108
- let verified: Awaited<ReturnType<typeof provider.verifyAccessToken>>
109
- try {
110
- verified = await provider.verifyAccessToken(refreshed.accessToken)
111
- } catch (err) {
112
- clearSessionCookies()
113
- throw ERR_UNAUTHENTICATED({
114
- message: 'refreshed access token did not verify',
115
- cause: err,
116
- })
117
- }
118
-
119
- return {
120
- actor: verified.actor,
121
- requestId: uuidv7(),
122
- readMode: 'any',
123
- }
124
- }
@@ -1,113 +0,0 @@
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
- /**
10
- * Cookie helpers for admin session management.
11
- *
12
- * Two separate httpOnly cookies, one for each token half:
13
- *
14
- * - `byline_access_token` — short-lived JWT; sent on every request.
15
- * - `byline_refresh_token` — long-lived opaque string; sent back only
16
- * when the access cookie is missing or
17
- * expired, to mint a new access token.
18
- *
19
- * Presenting "one session" to the user is a function of the middleware —
20
- * `getAdminRequestContext()` transparently refreshes the access cookie
21
- * using the refresh cookie, so the UI layer doesn't have to care.
22
- *
23
- * All cookies set here use:
24
- * - `httpOnly: true` — inaccessible to JavaScript (XSS-hardened).
25
- * - `sameSite: 'lax'` — sent on top-level navigations but not
26
- * cross-origin subrequests (reasonable default
27
- * for an admin SSR app).
28
- * - `secure: true` in production — https-only; dev keeps it off so
29
- * cookies work on http://localhost.
30
- * - `path: '/'` — available everywhere in the app.
31
- */
32
-
33
- import { getCookie, setCookie } from '@tanstack/react-start/server'
34
-
35
- export const ACCESS_TOKEN_COOKIE = 'byline_access_token'
36
- export const REFRESH_TOKEN_COOKIE = 'byline_refresh_token'
37
-
38
- const IS_PROD = process.env.NODE_ENV === 'production'
39
-
40
- /** Read the access-token cookie. Returns undefined when not present. */
41
- export function readAccessTokenCookie(): string | undefined {
42
- return getCookie(ACCESS_TOKEN_COOKIE)
43
- }
44
-
45
- /** Read the refresh-token cookie. Returns undefined when not present. */
46
- export function readRefreshTokenCookie(): string | undefined {
47
- return getCookie(REFRESH_TOKEN_COOKIE)
48
- }
49
-
50
- export interface SessionCookieTokens {
51
- accessToken: string
52
- refreshToken: string
53
- accessTokenExpiresAt: Date
54
- refreshTokenExpiresAt: Date
55
- }
56
-
57
- /**
58
- * Write both access and refresh cookies. Called after sign-in and after
59
- * every transparent refresh in `getAdminRequestContext()`.
60
- *
61
- * `maxAge` is derived from each token's own expiry claim so the browser
62
- * drops the cookies at the same moment the server would reject them —
63
- * saves round trips when the refresh token has fully expired.
64
- */
65
- export function setSessionCookies(tokens: SessionCookieTokens): void {
66
- const now = Date.now()
67
- const accessMaxAgeSeconds = Math.max(
68
- 0,
69
- Math.floor((tokens.accessTokenExpiresAt.getTime() - now) / 1000)
70
- )
71
- const refreshMaxAgeSeconds = Math.max(
72
- 0,
73
- Math.floor((tokens.refreshTokenExpiresAt.getTime() - now) / 1000)
74
- )
75
-
76
- setCookie(ACCESS_TOKEN_COOKIE, tokens.accessToken, {
77
- httpOnly: true,
78
- sameSite: 'lax',
79
- secure: IS_PROD,
80
- path: '/',
81
- maxAge: accessMaxAgeSeconds,
82
- })
83
-
84
- setCookie(REFRESH_TOKEN_COOKIE, tokens.refreshToken, {
85
- httpOnly: true,
86
- sameSite: 'lax',
87
- secure: IS_PROD,
88
- path: '/',
89
- maxAge: refreshMaxAgeSeconds,
90
- })
91
- }
92
-
93
- /**
94
- * Clear both session cookies. Called on sign-out and on any auth failure
95
- * during `getAdminRequestContext()` (ensures the browser does not keep
96
- * trying a token that the server has already rejected).
97
- */
98
- export function clearSessionCookies(): void {
99
- setCookie(ACCESS_TOKEN_COOKIE, '', {
100
- httpOnly: true,
101
- sameSite: 'lax',
102
- secure: IS_PROD,
103
- path: '/',
104
- maxAge: 0,
105
- })
106
- setCookie(REFRESH_TOKEN_COOKIE, '', {
107
- httpOnly: true,
108
- sameSite: 'lax',
109
- secure: IS_PROD,
110
- path: '/',
111
- maxAge: 0,
112
- })
113
- }
@@ -1,73 +0,0 @@
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
- /**
10
- * Cookie helpers for the front-end *preview mode* toggle.
11
- *
12
- * Preview mode lets a signed-in admin see draft (and other non-published)
13
- * versions on the public host pages without changing any markup or route
14
- * structure. The mechanic is intentionally narrow:
15
- *
16
- * - A single httpOnly cookie (`byline_preview=1`) carries the user's
17
- * "show me drafts" intent across requests within the same browser
18
- * session.
19
- * - The cookie's mere *presence* is the signal — there is no payload to
20
- * verify. The actual safety check is the admin session: when the
21
- * viewer client (or a server fn) sees the cookie, it tries to resolve
22
- * `getAdminRequestContext()` and only elevates the read mode when a
23
- * valid admin session resolves. A stale preview cookie on an
24
- * anonymous browser falls through to public/published reads silently.
25
- *
26
- * Cookie attributes:
27
- * - `httpOnly: true` — inaccessible to JS; toggled only via server fns.
28
- * - `sameSite: 'lax'` — sent on top-level navigations only.
29
- * - `secure: true` in production — https-only.
30
- * - `path: '/'` — visible to every public route.
31
- * - `maxAge: 1 day` — preview is meant to be a short-lived editorial
32
- * mode, not a permanent state. Admins can
33
- * re-enable from the admin UI when needed.
34
- */
35
-
36
- import { getCookie, setCookie } from '@tanstack/react-start/server'
37
-
38
- export const PREVIEW_COOKIE = 'byline_preview'
39
- const PREVIEW_MAX_AGE_SECONDS = 60 * 60 * 24 // 1 day
40
-
41
- const IS_PROD = process.env.NODE_ENV === 'production'
42
-
43
- /** True iff the preview cookie is currently set on the request. */
44
- export function readPreviewCookie(): boolean {
45
- return getCookie(PREVIEW_COOKIE) === '1'
46
- }
47
-
48
- /**
49
- * Write the preview cookie. Callers should ensure the request is from a
50
- * valid admin session before invoking — the cookie itself is just a flag
51
- * and carries no proof of authorisation. See `enablePreviewModeFn` for
52
- * the canonical entry point.
53
- */
54
- export function setPreviewCookie(): void {
55
- setCookie(PREVIEW_COOKIE, '1', {
56
- httpOnly: true,
57
- sameSite: 'lax',
58
- secure: IS_PROD,
59
- path: '/',
60
- maxAge: PREVIEW_MAX_AGE_SECONDS,
61
- })
62
- }
63
-
64
- /** Clear the preview cookie. Safe to call from any context. */
65
- export function clearPreviewCookie(): void {
66
- setCookie(PREVIEW_COOKIE, '', {
67
- httpOnly: true,
68
- sameSite: 'lax',
69
- secure: IS_PROD,
70
- path: '/',
71
- maxAge: 0,
72
- })
73
- }
@@ -1,66 +0,0 @@
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
- /**
10
- * The admin webapp's `BylineClient` singleton.
11
- *
12
- * Configured with a `requestContext` factory that resolves the
13
- * authenticated admin actor per call via `getAdminRequestContext`. Every
14
- * admin server fn that reads documents goes through this client, so the
15
- * full read pipeline (`beforeRead` → `findDocuments` → `populate` →
16
- * `afterRead`) is uniform between admin and any future external
17
- * client. The collection runtime, db adapter, and storage provider are
18
- * sourced from the shared `getServerConfig()` so we don't duplicate
19
- * adapter wiring.
20
- */
21
-
22
- import { createSuperAdminContext } from '@byline/auth'
23
- import { type BylineClient, createBylineClient } from '@byline/client'
24
- import { getServerConfig } from '@byline/core'
25
-
26
- import { getAdminRequestContext } from '../auth/auth-context.js'
27
-
28
- let cachedClient: BylineClient | undefined
29
-
30
- export function getAdminBylineClient(): BylineClient {
31
- if (cachedClient) return cachedClient
32
- cachedClient = createBylineClient({
33
- config: getServerConfig(),
34
- // Resolved per-call so each server fn picks up the actor from the
35
- // current request's session cookies. `getAdminRequestContext` runs the
36
- // refresh dance on its own and throws `ERR_UNAUTHENTICATED` when no
37
- // session is present — the client surfaces the throw verbatim.
38
- requestContext: getAdminRequestContext,
39
- })
40
- return cachedClient
41
- }
42
-
43
- let cachedSystemClient: BylineClient | undefined
44
-
45
- /**
46
- * A `BylineClient` bound to an explicit super-admin context — for
47
- * **system / background** work that is not scoped to an HTTP request:
48
- * lifecycle-hook search indexing, maintenance scripts, seeds, migrations.
49
- *
50
- * Unlike {@link getAdminBylineClient}, this does **not** read session
51
- * cookies, so it works outside the TanStack Start server runtime (a bare
52
- * `tsx` script, a seed, a test). Reaching for the request-scoped client in
53
- * a lifecycle hook couples that hook to the live server and throws
54
- * `No StartEvent found in AsyncLocalStorage` from any out-of-band write
55
- * path. Indexing is maintenance, not a user action — it reads the published
56
- * view and bypasses `beforeRead` — so the super-admin context is both
57
- * correct and runtime-agnostic. The context is auditable by its stable id.
58
- */
59
- export function getSystemBylineClient(): BylineClient {
60
- if (cachedSystemClient) return cachedSystemClient
61
- cachedSystemClient = createBylineClient({
62
- config: getServerConfig(),
63
- requestContext: createSuperAdminContext({ id: 'byline-system-client' }),
64
- })
65
- return cachedSystemClient
66
- }
@@ -1,53 +0,0 @@
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
- /**
10
- * The host's *public-read* `BylineClient` singleton — the strict
11
- * "preview-can-never-apply" sibling of `getViewerBylineClient`.
12
- *
13
- * Configured with `actor: null` and `readMode: 'published'`, full stop.
14
- * The `requestContext` factory does not consult the `byline_preview`
15
- * cookie or attempt any admin-session resolution, so an admin browsing
16
- * with preview mode on cannot accidentally elevate a read that flows
17
- * through this client. `assertActorCanPerform` permits the null actor
18
- * exactly on `read` with `readMode === 'published'`, which is what the
19
- * factory always returns.
20
- *
21
- * **Use this client for:**
22
- * - RSS feeds, Atom feeds, sitemaps
23
- * - JSON or other endpoints exposed to third-party consumers
24
- * - any response that an upstream CDN / cache might serve to multiple
25
- * visitors without keying off the `byline_preview` cookie
26
- *
27
- * **Use `getViewerBylineClient` instead for:**
28
- * - user-facing public pages where an admin's preview-mode session
29
- * should be honoured
30
- *
31
- * Both clients share the same module-scoped singleton pattern so the
32
- * SDK's per-instance `collectionRecordCache` is amortised across the
33
- * process lifetime.
34
- *
35
- * Companion: `getAdminBylineClient` in `./byline-client.ts` for the
36
- * admin webapp's authenticated reads (resolves a fresh `RequestContext`
37
- * from session cookies on every call).
38
- */
39
-
40
- import { createRequestContext } from '@byline/auth'
41
- import { type BylineClient, createBylineClient } from '@byline/client'
42
- import { getServerConfig } from '@byline/core'
43
-
44
- let cachedClient: BylineClient | undefined
45
-
46
- export function getPublicBylineClient(): BylineClient {
47
- if (cachedClient) return cachedClient
48
- cachedClient = createBylineClient({
49
- config: getServerConfig(),
50
- requestContext: () => createRequestContext({ readMode: 'published' }),
51
- })
52
- return cachedClient
53
- }