@byline/host-tanstack-start 4.0.0 → 4.2.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 (136) hide show
  1. package/dist/admin-shell/collections/tree-list-projection.d.ts +1 -1
  2. package/dist/i18n/resolve-locale.js +1 -1
  3. package/dist/index.d.ts +5 -6
  4. package/dist/integrations/host-bridge.d.ts +13 -0
  5. package/dist/integrations/host-bridge.js +22 -0
  6. package/dist/integrations/serve-uploads.d.ts +1 -0
  7. package/dist/integrations/serve-uploads.js +1 -0
  8. package/dist/server-fns/admin-account/change-password.js +1 -1
  9. package/dist/server-fns/admin-account/get.js +1 -1
  10. package/dist/server-fns/admin-account/update.js +1 -1
  11. package/dist/server-fns/admin-activity/get.js +1 -1
  12. package/dist/server-fns/admin-permissions/get-role-abilities.js +1 -1
  13. package/dist/server-fns/admin-permissions/list-registered.js +1 -1
  14. package/dist/server-fns/admin-permissions/set-role-abilities.js +1 -1
  15. package/dist/server-fns/admin-permissions/who-has.js +1 -1
  16. package/dist/server-fns/admin-roles/create.js +1 -1
  17. package/dist/server-fns/admin-roles/delete.js +1 -1
  18. package/dist/server-fns/admin-roles/get.js +1 -1
  19. package/dist/server-fns/admin-roles/list.js +1 -1
  20. package/dist/server-fns/admin-roles/reorder.js +1 -1
  21. package/dist/server-fns/admin-roles/update.js +1 -1
  22. package/dist/server-fns/admin-users/create.js +1 -1
  23. package/dist/server-fns/admin-users/delete.js +1 -1
  24. package/dist/server-fns/admin-users/disable.js +1 -1
  25. package/dist/server-fns/admin-users/enable.js +1 -1
  26. package/dist/server-fns/admin-users/get-user-roles.js +1 -1
  27. package/dist/server-fns/admin-users/get.js +1 -1
  28. package/dist/server-fns/admin-users/list.js +1 -1
  29. package/dist/server-fns/admin-users/set-password.js +1 -1
  30. package/dist/server-fns/admin-users/set-user-roles.js +1 -1
  31. package/dist/server-fns/admin-users/update.js +1 -1
  32. package/dist/server-fns/ai/execute.js +1 -1
  33. package/dist/server-fns/auth/current-user.js +1 -1
  34. package/dist/server-fns/auth/sign-in.js +1 -1
  35. package/dist/server-fns/auth/sign-out.js +1 -2
  36. package/dist/server-fns/collections/audit.js +1 -1
  37. package/dist/server-fns/collections/copy-to-locale.js +1 -1
  38. package/dist/server-fns/collections/create.js +1 -1
  39. package/dist/server-fns/collections/delete-locale.js +1 -1
  40. package/dist/server-fns/collections/delete.js +1 -1
  41. package/dist/server-fns/collections/duplicate.js +1 -1
  42. package/dist/server-fns/collections/get.js +1 -1
  43. package/dist/server-fns/collections/history.js +1 -1
  44. package/dist/server-fns/collections/list.js +1 -1
  45. package/dist/server-fns/collections/reindex.js +1 -1
  46. package/dist/server-fns/collections/reorder.js +1 -1
  47. package/dist/server-fns/collections/restore-version.js +1 -1
  48. package/dist/server-fns/collections/stats.js +1 -1
  49. package/dist/server-fns/collections/status.js +1 -1
  50. package/dist/server-fns/collections/tree.js +1 -1
  51. package/dist/server-fns/collections/update.js +1 -1
  52. package/dist/server-fns/collections/upload.js +2 -1
  53. package/dist/server-fns/i18n/set-locale.js +1 -1
  54. package/dist/server-fns/preview/disable.js +1 -1
  55. package/dist/server-fns/preview/enable.js +1 -2
  56. package/dist/server-fns/preview/state.js +1 -1
  57. package/package.json +8 -13
  58. package/src/admin-shell/collections/tree-list-projection.ts +1 -1
  59. package/src/i18n/resolve-locale.ts +1 -1
  60. package/src/index.ts +5 -6
  61. package/src/integrations/host-bridge.ts +56 -0
  62. package/src/integrations/serve-uploads.ts +6 -0
  63. package/src/server-fns/admin-account/change-password.ts +1 -1
  64. package/src/server-fns/admin-account/get.ts +1 -1
  65. package/src/server-fns/admin-account/update.ts +1 -1
  66. package/src/server-fns/admin-activity/get.ts +1 -1
  67. package/src/server-fns/admin-permissions/get-role-abilities.ts +1 -1
  68. package/src/server-fns/admin-permissions/list-registered.ts +1 -1
  69. package/src/server-fns/admin-permissions/set-role-abilities.ts +1 -1
  70. package/src/server-fns/admin-permissions/who-has.ts +1 -1
  71. package/src/server-fns/admin-roles/create.ts +1 -1
  72. package/src/server-fns/admin-roles/delete.ts +1 -1
  73. package/src/server-fns/admin-roles/get.ts +1 -1
  74. package/src/server-fns/admin-roles/list.ts +1 -1
  75. package/src/server-fns/admin-roles/reorder.ts +1 -1
  76. package/src/server-fns/admin-roles/update.ts +1 -1
  77. package/src/server-fns/admin-users/create.ts +1 -1
  78. package/src/server-fns/admin-users/delete.ts +1 -1
  79. package/src/server-fns/admin-users/disable.ts +1 -1
  80. package/src/server-fns/admin-users/enable.ts +1 -1
  81. package/src/server-fns/admin-users/get-user-roles.ts +1 -1
  82. package/src/server-fns/admin-users/get.ts +1 -1
  83. package/src/server-fns/admin-users/list.ts +1 -1
  84. package/src/server-fns/admin-users/set-password.ts +1 -1
  85. package/src/server-fns/admin-users/set-user-roles.ts +1 -1
  86. package/src/server-fns/admin-users/update.ts +1 -1
  87. package/src/server-fns/ai/execute.ts +1 -2
  88. package/src/server-fns/auth/current-user.ts +1 -1
  89. package/src/server-fns/auth/sign-in.ts +1 -1
  90. package/src/server-fns/auth/sign-out.ts +5 -3
  91. package/src/server-fns/collections/audit.ts +1 -1
  92. package/src/server-fns/collections/copy-to-locale.ts +1 -1
  93. package/src/server-fns/collections/create.ts +1 -1
  94. package/src/server-fns/collections/delete-locale.ts +1 -1
  95. package/src/server-fns/collections/delete.ts +1 -1
  96. package/src/server-fns/collections/duplicate.ts +1 -1
  97. package/src/server-fns/collections/get.ts +1 -1
  98. package/src/server-fns/collections/history.ts +1 -1
  99. package/src/server-fns/collections/list.ts +1 -1
  100. package/src/server-fns/collections/reindex.ts +1 -1
  101. package/src/server-fns/collections/reorder.ts +1 -1
  102. package/src/server-fns/collections/restore-version.ts +1 -1
  103. package/src/server-fns/collections/stats.ts +2 -1
  104. package/src/server-fns/collections/status.ts +1 -1
  105. package/src/server-fns/collections/tree.ts +2 -2
  106. package/src/server-fns/collections/update.ts +1 -1
  107. package/src/server-fns/collections/upload.ts +2 -1
  108. package/src/server-fns/i18n/set-locale.ts +1 -1
  109. package/src/server-fns/preview/disable.ts +1 -1
  110. package/src/server-fns/preview/enable.ts +1 -2
  111. package/src/server-fns/preview/state.ts +1 -1
  112. package/dist/auth/auth-context.d.ts +0 -50
  113. package/dist/auth/auth-context.js +0 -61
  114. package/dist/auth/auth-cookies.d.ts +0 -34
  115. package/dist/auth/auth-cookies.js +0 -46
  116. package/dist/auth/preview-cookies.d.ts +0 -19
  117. package/dist/auth/preview-cookies.js +0 -26
  118. package/dist/auth/request-scope.d.ts +0 -16
  119. package/dist/auth/request-scope.js +0 -24
  120. package/dist/integrations/byline-client.d.ts +0 -24
  121. package/dist/integrations/byline-client.js +0 -25
  122. package/dist/integrations/byline-public-client.d.ts +0 -47
  123. package/dist/integrations/byline-public-client.js +0 -19
  124. package/dist/integrations/byline-viewer-client.d.ts +0 -26
  125. package/dist/integrations/byline-viewer-client.js +0 -44
  126. package/src/auth/auth-context.test.node.ts +0 -302
  127. package/src/auth/auth-context.ts +0 -142
  128. package/src/auth/auth-cookies.ts +0 -113
  129. package/src/auth/preview-cookies.ts +0 -73
  130. package/src/auth/request-scope.test.node.ts +0 -93
  131. package/src/auth/request-scope.ts +0 -62
  132. package/src/integrations/byline-client.ts +0 -68
  133. package/src/integrations/byline-public-client.ts +0 -67
  134. package/src/integrations/byline-viewer-client.ts +0 -111
  135. package/src/integrations/request-context-contract.test.node.ts +0 -131
  136. package/src/integrations/request-context-factories.test.node.ts +0 -140
@@ -1,93 +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 per-request memoization. Mocks the Start runtime's
11
- * `getRequest` so "which request am I in" can be simulated: a stable
12
- * object per request, or a throw when running outside a request.
13
- */
14
-
15
- import { beforeEach, describe, expect, it, vi } from 'vitest'
16
-
17
- const mocks = vi.hoisted(() => ({
18
- getRequest: vi.fn(),
19
- }))
20
-
21
- vi.mock('@tanstack/react-start/server', () => ({
22
- getRequest: mocks.getRequest,
23
- }))
24
-
25
- import { oncePerRequest } from './request-scope.js'
26
-
27
- describe('oncePerRequest', () => {
28
- beforeEach(() => {
29
- vi.clearAllMocks()
30
- mocks.getRequest.mockImplementation(() => {
31
- throw new Error('No StartEvent found in AsyncLocalStorage')
32
- })
33
- })
34
-
35
- it('resolves the factory once per request per key', async () => {
36
- mocks.getRequest.mockReturnValue({ id: 'request-a' })
37
- const factory = vi.fn(async () => ({ token: Math.random() }))
38
-
39
- const first = await oncePerRequest('ctx', factory)
40
- const second = await oncePerRequest('ctx', factory)
41
-
42
- expect(second).toBe(first)
43
- expect(factory).toHaveBeenCalledTimes(1)
44
- })
45
-
46
- it('keys memoization by request identity', async () => {
47
- const requestA = { id: 'a' }
48
- const requestB = { id: 'b' }
49
- const factory = vi.fn(async () => ({ token: Math.random() }))
50
-
51
- mocks.getRequest.mockReturnValue(requestA)
52
- const first = await oncePerRequest('ctx', factory)
53
- mocks.getRequest.mockReturnValue(requestB)
54
- const second = await oncePerRequest('ctx', factory)
55
-
56
- expect(second).not.toBe(first)
57
- expect(factory).toHaveBeenCalledTimes(2)
58
- })
59
-
60
- it('isolates entries by key within one request', async () => {
61
- mocks.getRequest.mockReturnValue({ id: 'request-a' })
62
- const admin = vi.fn(async () => 'admin-context')
63
- const publicCtx = vi.fn(async () => 'public-context')
64
-
65
- await expect(oncePerRequest('admin', admin)).resolves.toBe('admin-context')
66
- await expect(oncePerRequest('public', publicCtx)).resolves.toBe('public-context')
67
- await expect(oncePerRequest('admin', admin)).resolves.toBe('admin-context')
68
-
69
- expect(admin).toHaveBeenCalledTimes(1)
70
- expect(publicCtx).toHaveBeenCalledTimes(1)
71
- })
72
-
73
- it('memoizes rejections for the remainder of the request', async () => {
74
- mocks.getRequest.mockReturnValue({ id: 'request-a' })
75
- const factory = vi.fn(async () => {
76
- throw new Error('no admin session')
77
- })
78
-
79
- await expect(oncePerRequest('ctx', factory)).rejects.toThrow('no admin session')
80
- await expect(oncePerRequest('ctx', factory)).rejects.toThrow('no admin session')
81
- expect(factory).toHaveBeenCalledTimes(1)
82
- })
83
-
84
- it('runs unmemoized outside the Start runtime', async () => {
85
- const factory = vi.fn(async () => ({ token: Math.random() }))
86
-
87
- const first = await oncePerRequest('ctx', factory)
88
- const second = await oncePerRequest('ctx', factory)
89
-
90
- expect(second).not.toBe(first)
91
- expect(factory).toHaveBeenCalledTimes(2)
92
- })
93
- })
@@ -1,62 +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
- * Per-request memoization for `RequestContext` factories.
11
- *
12
- * `RequestContext.requestId` is documented as "per logical request", and
13
- * the read-authorization layer binds every `ReadContext` to one immutable
14
- * request authority whose token includes that id. A factory that mints a
15
- * fresh context per call breaks both contracts as soon as two reads share
16
- * a `ReadContext` (the admin tree view, hook-threaded nested reads):
17
- * the second bind sees a different authority token and throws
18
- * 'ReadContext cannot be reused across request authorities'.
19
- *
20
- * Memoizing on the current Start request makes every context factory
21
- * request-stable. It also means at most one session verification — and at
22
- * most one refresh-token rotation — per request, instead of one per call.
23
- */
24
-
25
- import { getRequest } from '@tanstack/react-start/server'
26
-
27
- const requestScopes = new WeakMap<object, Map<string, Promise<unknown>>>()
28
-
29
- /**
30
- * Resolve `factory` at most once per HTTP request for a given `key`. The
31
- * returned promise is memoized on the current Start request — rejections
32
- * included, since a request's cookies cannot change mid-flight and retrying
33
- * a failed refresh would burn a second rotation on an already-rotated
34
- * token. Outside the Start runtime (seed scripts, unit tests, background
35
- * jobs) there is no request to key on and the factory runs unmemoized.
36
- */
37
- export function oncePerRequest<T>(key: string, factory: () => Promise<T>): Promise<T> {
38
- const request = currentRequest()
39
- if (!request) return factory()
40
-
41
- let scope = requestScopes.get(request)
42
- if (!scope) {
43
- scope = new Map()
44
- requestScopes.set(request, scope)
45
- }
46
-
47
- const existing = scope.get(key)
48
- if (existing) return existing as Promise<T>
49
-
50
- const pending = factory()
51
- scope.set(key, pending)
52
- return pending
53
- }
54
-
55
- function currentRequest(): object | undefined {
56
- try {
57
- return getRequest()
58
- } catch {
59
- // No StartEvent in AsyncLocalStorage — running outside a request.
60
- return undefined
61
- }
62
- }
@@ -1,68 +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 from the current request's session cookies, memoized per
35
- // request: every read in one request binds the same context instance
36
- // (and requestId), which the ReadContext authority check requires.
37
- // `getAdminRequestContext` runs the refresh dance on its own and throws
38
- // `ERR_UNAUTHENTICATED` when no session is present — the client
39
- // surfaces the throw verbatim.
40
- requestContext: getAdminRequestContext,
41
- })
42
- return cachedClient
43
- }
44
-
45
- let cachedSystemClient: BylineClient | undefined
46
-
47
- /**
48
- * A `BylineClient` bound to an explicit super-admin context — for
49
- * **system / background** work that is not scoped to an HTTP request:
50
- * lifecycle-hook search indexing, maintenance scripts, seeds, migrations.
51
- *
52
- * Unlike {@link getAdminBylineClient}, this does **not** read session
53
- * cookies, so it works outside the TanStack Start server runtime (a bare
54
- * `tsx` script, a seed, a test). Reaching for the request-scoped client in
55
- * a lifecycle hook couples that hook to the live server and throws
56
- * `No StartEvent found in AsyncLocalStorage` from any out-of-band write
57
- * path. Indexing is maintenance, not a user action — it reads the published
58
- * view and bypasses `beforeRead` — so the super-admin context is both
59
- * correct and runtime-agnostic. The context is auditable by its stable id.
60
- */
61
- export function getSystemBylineClient(): BylineClient {
62
- if (cachedSystemClient) return cachedSystemClient
63
- cachedSystemClient = createBylineClient({
64
- config: getServerConfig(),
65
- requestContext: createSuperAdminContext({ id: 'byline-system-client' }),
66
- })
67
- return cachedSystemClient
68
- }
@@ -1,67 +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 the `RequestContext`
37
- * from session cookies, memoized per request).
38
- */
39
-
40
- import { createRequestContext, type RequestContext } from '@byline/auth'
41
- import { type BylineClient, createBylineClient } from '@byline/client'
42
- import { getServerConfig } from '@byline/core'
43
-
44
- import { oncePerRequest } from '../auth/request-scope.js'
45
-
46
- /**
47
- * Anonymous, published-only context — memoized per request so the
48
- * context (and its requestId) is stable across every read in one
49
- * request: reads sharing a ReadContext must bind a single request
50
- * authority.
51
- */
52
- export function resolvePublicRequestContext(): Promise<RequestContext> {
53
- return oncePerRequest('byline:public-request-context', async () =>
54
- createRequestContext({ readMode: 'published' })
55
- )
56
- }
57
-
58
- let cachedClient: BylineClient | undefined
59
-
60
- export function getPublicBylineClient(): BylineClient {
61
- if (cachedClient) return cachedClient
62
- cachedClient = createBylineClient({
63
- config: getServerConfig(),
64
- requestContext: resolvePublicRequestContext,
65
- })
66
- return cachedClient
67
- }
@@ -1,111 +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 *viewer* `BylineClient` singleton — the preview-aware
11
- * sibling of `getPublicBylineClient` (`./byline-public-client.ts`).
12
- * Behaves identically to the public client until both the
13
- * `byline_preview` cookie is set **and** a valid admin session resolves;
14
- * at that point the per-call `requestContext` factory returns the
15
- * authenticated `AdminAuth` instead of the anonymous null actor.
16
- *
17
- * Why this matters: `assertActorCanPerform` (packages/core/src/auth) only
18
- * permits anonymous reads when `readMode === 'published'`. So if a server
19
- * fn passes `status: 'any'` to surface drafts, an anonymous request
20
- * trips the gate and throws — but a preview-mode admin sails through. The
21
- * source-view selection itself remains a per-call decision: the SDK's
22
- * `resolveReadMode` defaults to `'published'` regardless of context, so
23
- * every server fn still has to opt in by passing `status: 'any'`.
24
- *
25
- * The contract for callers:
26
- *
27
- * 1. Use `getViewerBylineClient()` on any user-facing public read
28
- * where an admin's preview-mode session should be honoured. Use
29
- * `getPublicBylineClient()` instead for endpoints where preview
30
- * should never apply (RSS feeds, sitemaps, third-party endpoints,
31
- * anything an upstream cache might serve without keying off the
32
- * preview cookie).
33
- * 2. Call `isPreviewActive()` once per server fn to decide whether to
34
- * pass `status: 'any'` on the read.
35
- *
36
- * Stale cookies fail closed: if the preview cookie is present but no
37
- * valid admin session resolves, the factory returns the same anonymous
38
- * + `'published'` context the public client would have used, so the
39
- * worst case is "preview cookie does nothing".
40
- *
41
- * The collection-record cache is per-instance, so a singleton here keeps
42
- * the same amortisation benefit `getPublicBylineClient` has — the two
43
- * helpers are parallel singletons (one cached `BylineClient` each).
44
- */
45
-
46
- import { createRequestContext } from '@byline/auth'
47
- import { type BylineClient, createBylineClient } from '@byline/client'
48
- import { getServerConfig } from '@byline/core'
49
-
50
- import { getAdminRequestContext } from '../auth/auth-context.js'
51
- import { readPreviewCookie } from '../auth/preview-cookies.js'
52
- import { oncePerRequest } from '../auth/request-scope.js'
53
-
54
- let cachedClient: BylineClient | undefined
55
-
56
- export function getViewerBylineClient(): BylineClient {
57
- if (cachedClient) return cachedClient
58
- cachedClient = createBylineClient({
59
- config: getServerConfig(),
60
- requestContext: resolveViewerRequestContext,
61
- })
62
- return cachedClient
63
- }
64
-
65
- /**
66
- * Preview-aware viewer context — memoized per request so every read in
67
- * one request binds the same context instance (and requestId): reads
68
- * sharing a ReadContext must resolve a single request authority.
69
- */
70
- export function resolveViewerRequestContext() {
71
- return oncePerRequest('byline:viewer-request-context', resolveViewerContext)
72
- }
73
-
74
- async function resolveViewerContext() {
75
- // No preview cookie → behave exactly like the public client.
76
- // Cheap path; no JWT verification, no DB lookup.
77
- if (!readPreviewCookie()) {
78
- return createRequestContext({ readMode: 'published' })
79
- }
80
-
81
- // Preview cookie present → try the admin context. A failure means
82
- // the cookie is stale (admin signed out, session expired, refresh
83
- // rejected). We swallow the error and fall back to the public
84
- // context so the page renders instead of erroring.
85
- try {
86
- const ctx = await getAdminRequestContext()
87
- return { ...ctx, readMode: 'any' as const }
88
- } catch {
89
- return createRequestContext({ readMode: 'published' })
90
- }
91
- }
92
-
93
- /**
94
- * Resolve whether the current request should surface non-published
95
- * versions. True iff the preview cookie is set **and** a valid admin
96
- * session resolves. Server fns call this once and pass the result to
97
- * `status: preview ? 'any' : 'published'` on the SDK read.
98
- *
99
- * Defensive by design: the cookie alone is not enough. A signed-out
100
- * browser carrying an old preview cookie still gets `false` here, which
101
- * keeps stray query strings or shared links from leaking drafts.
102
- */
103
- export async function isPreviewActive(): Promise<boolean> {
104
- if (!readPreviewCookie()) return false
105
- try {
106
- await getAdminRequestContext()
107
- return true
108
- } catch {
109
- return false
110
- }
111
- }
@@ -1,131 +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
- * Cross-layer contract test: a real `BylineClient` wired to the real
11
- * cookie-derived host factory (`getAdminRequestContext`) under a mocked
12
- * Start request, running two operations that share one `ReadContext`.
13
- *
14
- * This is the seam every per-layer suite missed when the admin tree view
15
- * broke with 'ReadContext cannot be reused across request authorities':
16
- * client tests used stable test contexts, host auth tests called the
17
- * factory once, and tree tests mocked the handle. Here the factory's
18
- * request-stability contract (see `BylineClientConfig.requestContext`)
19
- * is exercised end-to-end through the SDK's authority binding.
20
- */
21
-
22
- import { AdminAuth } from '@byline/auth'
23
- import { createBylineClient } from '@byline/client'
24
- import type { CollectionDefinition, IDbAdapter } from '@byline/core'
25
- import { beforeEach, describe, expect, it, vi } from 'vitest'
26
-
27
- const mocks = vi.hoisted(() => ({
28
- verifyAccessToken: vi.fn(),
29
- refreshSession: vi.fn(),
30
- getCookie: vi.fn(),
31
- setCookie: vi.fn(),
32
- getRequestHeader: vi.fn(),
33
- getRequest: vi.fn(),
34
- }))
35
-
36
- vi.mock('@byline/core', async () => {
37
- const actual = await vi.importActual<typeof import('@byline/core')>('@byline/core')
38
- return {
39
- ...actual,
40
- getServerConfig: () => ({
41
- sessionProvider: {
42
- verifyAccessToken: mocks.verifyAccessToken,
43
- refreshSession: mocks.refreshSession,
44
- },
45
- }),
46
- }
47
- })
48
-
49
- vi.mock('@tanstack/react-start/server', () => ({
50
- getCookie: mocks.getCookie,
51
- setCookie: mocks.setCookie,
52
- getRequestHeader: mocks.getRequestHeader,
53
- getRequest: mocks.getRequest,
54
- }))
55
-
56
- import { createReadContext } from '@byline/core'
57
-
58
- import { getAdminRequestContext } from '../auth/auth-context.js'
59
-
60
- const postsCollection: CollectionDefinition = {
61
- path: 'posts',
62
- labels: { singular: 'Post', plural: 'Posts' },
63
- fields: [{ name: 'title', type: 'text', label: 'Title' }],
64
- }
65
-
66
- function mockDb(): IDbAdapter {
67
- return {
68
- commands: { collections: {}, documents: {}, counters: {} },
69
- queries: {
70
- collections: {
71
- getCollectionByPath: vi.fn().mockResolvedValue({ id: 'col-1', version: 1 }),
72
- },
73
- documents: {
74
- findDocuments: vi.fn().mockResolvedValue({ documents: [], total: 0 }),
75
- getDocumentsByDocumentIds: vi.fn().mockResolvedValue([]),
76
- },
77
- },
78
- } as unknown as IDbAdapter
79
- }
80
-
81
- describe('host factory ↔ client authority contract', () => {
82
- beforeEach(() => {
83
- vi.clearAllMocks()
84
- mocks.getCookie.mockImplementation((name: string) =>
85
- name === 'byline_access_token' ? 'valid-access' : undefined
86
- )
87
- mocks.verifyAccessToken.mockResolvedValue({
88
- actor: new AdminAuth({ id: 'admin-1', abilities: ['collections.posts.read'] }),
89
- })
90
- })
91
-
92
- it('two reads sharing a ReadContext bind one authority within a request', async () => {
93
- mocks.getRequest.mockReturnValue({ id: 'request-a' })
94
- const db = mockDb()
95
- const beforeRead = vi.fn(() => ({ title: 'scoped' }))
96
- const client = createBylineClient({
97
- db,
98
- collections: [{ ...postsCollection, hooks: { beforeRead } }],
99
- requestContext: getAdminRequestContext,
100
- })
101
- const readContext = createReadContext()
102
-
103
- // The admin tree view shape: two top-level reads, one shared ReadContext.
104
- await client.collection('posts').find({ status: 'any', _readContext: readContext })
105
- await client.collection('posts').find({ status: 'any', _readContext: readContext })
106
-
107
- expect(db.queries.documents.findDocuments).toHaveBeenCalledTimes(2)
108
- // One session verification and one beforeRead compile for the whole
109
- // request — the factory resolved a single request-stable context.
110
- expect(mocks.verifyAccessToken).toHaveBeenCalledTimes(1)
111
- expect(beforeRead).toHaveBeenCalledTimes(1)
112
- })
113
-
114
- it('still rejects a ReadContext carried across two different requests', async () => {
115
- const db = mockDb()
116
- const client = createBylineClient({
117
- db,
118
- collections: [{ ...postsCollection, hooks: { beforeRead: () => ({ title: 'scoped' }) } }],
119
- requestContext: getAdminRequestContext,
120
- })
121
- const readContext = createReadContext()
122
-
123
- mocks.getRequest.mockReturnValue({ id: 'request-a' })
124
- await client.collection('posts').find({ status: 'any', _readContext: readContext })
125
-
126
- mocks.getRequest.mockReturnValue({ id: 'request-b' })
127
- await expect(
128
- client.collection('posts').find({ status: 'any', _readContext: readContext })
129
- ).rejects.toThrow('cannot be reused across request authorities')
130
- })
131
- })
@@ -1,140 +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
- * Direct regression coverage for the public and viewer request-context
11
- * factories: both must be request-stable (same instance, same requestId,
12
- * for every call within one Start request) so reads sharing a
13
- * `ReadContext` bind a single request authority.
14
- */
15
-
16
- import { AdminAuth } from '@byline/auth'
17
- import { beforeEach, describe, expect, it, vi } from 'vitest'
18
-
19
- const mocks = vi.hoisted(() => ({
20
- verifyAccessToken: vi.fn(),
21
- refreshSession: vi.fn(),
22
- getCookie: vi.fn(),
23
- setCookie: vi.fn(),
24
- getRequestHeader: vi.fn(),
25
- getRequest: vi.fn(),
26
- }))
27
-
28
- vi.mock('@byline/core', async () => {
29
- const actual = await vi.importActual<typeof import('@byline/core')>('@byline/core')
30
- return {
31
- ...actual,
32
- getServerConfig: () => ({
33
- sessionProvider: {
34
- verifyAccessToken: mocks.verifyAccessToken,
35
- refreshSession: mocks.refreshSession,
36
- },
37
- }),
38
- }
39
- })
40
-
41
- vi.mock('@tanstack/react-start/server', () => ({
42
- getCookie: mocks.getCookie,
43
- setCookie: mocks.setCookie,
44
- getRequestHeader: mocks.getRequestHeader,
45
- getRequest: mocks.getRequest,
46
- }))
47
-
48
- import { resolvePublicRequestContext } from './byline-public-client.js'
49
- import { resolveViewerRequestContext } from './byline-viewer-client.js'
50
-
51
- function cookiesReturn(values: Record<string, string | undefined>) {
52
- mocks.getCookie.mockImplementation((name: string) => values[name])
53
- }
54
-
55
- describe('request-context factories', () => {
56
- beforeEach(() => {
57
- vi.clearAllMocks()
58
- mocks.getRequest.mockImplementation(() => {
59
- throw new Error('No StartEvent found in AsyncLocalStorage')
60
- })
61
- cookiesReturn({})
62
- })
63
-
64
- describe('resolvePublicRequestContext', () => {
65
- it('returns the same anonymous published context for every call in one request', async () => {
66
- mocks.getRequest.mockReturnValue({ id: 'request-a' })
67
-
68
- const first = await resolvePublicRequestContext()
69
- const second = await resolvePublicRequestContext()
70
-
71
- expect(second).toBe(first)
72
- expect(first.actor).toBeNull()
73
- expect(first.readMode).toBe('published')
74
- expect(second.requestId).toBe(first.requestId)
75
- })
76
-
77
- it('resolves independently across requests', async () => {
78
- mocks.getRequest.mockReturnValue({ id: 'request-a' })
79
- const first = await resolvePublicRequestContext()
80
- mocks.getRequest.mockReturnValue({ id: 'request-b' })
81
- const second = await resolvePublicRequestContext()
82
-
83
- expect(second).not.toBe(first)
84
- expect(second.requestId).not.toBe(first.requestId)
85
- })
86
- })
87
-
88
- describe('resolveViewerRequestContext', () => {
89
- it('returns the same anonymous context per request when no preview cookie is set', async () => {
90
- mocks.getRequest.mockReturnValue({ id: 'request-a' })
91
-
92
- const first = await resolveViewerRequestContext()
93
- const second = await resolveViewerRequestContext()
94
-
95
- expect(second).toBe(first)
96
- expect(first.actor).toBeNull()
97
- expect(first.readMode).toBe('published')
98
- })
99
-
100
- it('returns the same admin any-mode context per request in preview mode', async () => {
101
- mocks.getRequest.mockReturnValue({ id: 'request-a' })
102
- cookiesReturn({ byline_preview: '1', byline_access_token: 'valid-access' })
103
- const actor = new AdminAuth({ id: 'admin-1', abilities: [] })
104
- mocks.verifyAccessToken.mockResolvedValue({ actor })
105
-
106
- const first = await resolveViewerRequestContext()
107
- const second = await resolveViewerRequestContext()
108
-
109
- expect(second).toBe(first)
110
- expect(first.actor).toBe(actor)
111
- expect(first.readMode).toBe('any')
112
- // The viewer context and the nested admin resolution are both
113
- // memoized — one session verification per request, not per call.
114
- expect(mocks.verifyAccessToken).toHaveBeenCalledTimes(1)
115
- })
116
-
117
- it('falls back to one stable anonymous context when the preview session is stale', async () => {
118
- mocks.getRequest.mockReturnValue({ id: 'request-a' })
119
- cookiesReturn({ byline_preview: '1', byline_access_token: 'stale' })
120
- mocks.verifyAccessToken.mockRejectedValue(new Error('expired'))
121
-
122
- const first = await resolveViewerRequestContext()
123
- const second = await resolveViewerRequestContext()
124
-
125
- expect(second).toBe(first)
126
- expect(first.actor).toBeNull()
127
- expect(first.readMode).toBe('published')
128
- })
129
-
130
- it('resolves independently across requests', async () => {
131
- mocks.getRequest.mockReturnValue({ id: 'request-a' })
132
- const first = await resolveViewerRequestContext()
133
- mocks.getRequest.mockReturnValue({ id: 'request-b' })
134
- const second = await resolveViewerRequestContext()
135
-
136
- expect(second).not.toBe(first)
137
- expect(second.requestId).not.toBe(first.requestId)
138
- })
139
- })
140
- })