@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.
- package/dist/admin-shell/collections/tree-list-projection.d.ts +1 -1
- package/dist/i18n/resolve-locale.js +1 -1
- package/dist/index.d.ts +5 -6
- package/dist/integrations/host-bridge.d.ts +13 -0
- package/dist/integrations/host-bridge.js +22 -0
- package/dist/integrations/serve-uploads.d.ts +1 -0
- package/dist/integrations/serve-uploads.js +1 -0
- package/dist/server-fns/admin-account/change-password.js +1 -1
- package/dist/server-fns/admin-account/get.js +1 -1
- package/dist/server-fns/admin-account/update.js +1 -1
- package/dist/server-fns/admin-activity/get.js +1 -1
- package/dist/server-fns/admin-permissions/get-role-abilities.js +1 -1
- package/dist/server-fns/admin-permissions/list-registered.js +1 -1
- package/dist/server-fns/admin-permissions/set-role-abilities.js +1 -1
- package/dist/server-fns/admin-permissions/who-has.js +1 -1
- package/dist/server-fns/admin-roles/create.js +1 -1
- package/dist/server-fns/admin-roles/delete.js +1 -1
- package/dist/server-fns/admin-roles/get.js +1 -1
- package/dist/server-fns/admin-roles/list.js +1 -1
- package/dist/server-fns/admin-roles/reorder.js +1 -1
- package/dist/server-fns/admin-roles/update.js +1 -1
- package/dist/server-fns/admin-users/create.js +1 -1
- package/dist/server-fns/admin-users/delete.js +1 -1
- package/dist/server-fns/admin-users/disable.js +1 -1
- package/dist/server-fns/admin-users/enable.js +1 -1
- package/dist/server-fns/admin-users/get-user-roles.js +1 -1
- package/dist/server-fns/admin-users/get.js +1 -1
- package/dist/server-fns/admin-users/list.js +1 -1
- package/dist/server-fns/admin-users/set-password.js +1 -1
- package/dist/server-fns/admin-users/set-user-roles.js +1 -1
- package/dist/server-fns/admin-users/update.js +1 -1
- package/dist/server-fns/ai/execute.js +1 -1
- package/dist/server-fns/auth/current-user.js +1 -1
- package/dist/server-fns/auth/sign-in.js +1 -1
- package/dist/server-fns/auth/sign-out.js +1 -2
- package/dist/server-fns/collections/audit.js +1 -1
- package/dist/server-fns/collections/copy-to-locale.js +1 -1
- package/dist/server-fns/collections/create.js +1 -1
- package/dist/server-fns/collections/delete-locale.js +1 -1
- package/dist/server-fns/collections/delete.js +1 -1
- package/dist/server-fns/collections/duplicate.js +1 -1
- package/dist/server-fns/collections/get.js +1 -1
- package/dist/server-fns/collections/history.js +1 -1
- package/dist/server-fns/collections/list.js +1 -1
- package/dist/server-fns/collections/reindex.js +1 -1
- package/dist/server-fns/collections/reorder.js +1 -1
- package/dist/server-fns/collections/restore-version.js +1 -1
- package/dist/server-fns/collections/stats.js +1 -1
- package/dist/server-fns/collections/status.js +1 -1
- package/dist/server-fns/collections/tree.js +1 -1
- package/dist/server-fns/collections/update.js +1 -1
- package/dist/server-fns/collections/upload.js +2 -1
- package/dist/server-fns/i18n/set-locale.js +1 -1
- package/dist/server-fns/preview/disable.js +1 -1
- package/dist/server-fns/preview/enable.js +1 -2
- package/dist/server-fns/preview/state.js +1 -1
- package/package.json +8 -13
- package/src/admin-shell/collections/tree-list-projection.ts +1 -1
- package/src/i18n/resolve-locale.ts +1 -1
- package/src/index.ts +5 -6
- package/src/integrations/host-bridge.ts +56 -0
- package/src/integrations/serve-uploads.ts +6 -0
- package/src/server-fns/admin-account/change-password.ts +1 -1
- package/src/server-fns/admin-account/get.ts +1 -1
- package/src/server-fns/admin-account/update.ts +1 -1
- package/src/server-fns/admin-activity/get.ts +1 -1
- package/src/server-fns/admin-permissions/get-role-abilities.ts +1 -1
- package/src/server-fns/admin-permissions/list-registered.ts +1 -1
- package/src/server-fns/admin-permissions/set-role-abilities.ts +1 -1
- package/src/server-fns/admin-permissions/who-has.ts +1 -1
- package/src/server-fns/admin-roles/create.ts +1 -1
- package/src/server-fns/admin-roles/delete.ts +1 -1
- package/src/server-fns/admin-roles/get.ts +1 -1
- package/src/server-fns/admin-roles/list.ts +1 -1
- package/src/server-fns/admin-roles/reorder.ts +1 -1
- package/src/server-fns/admin-roles/update.ts +1 -1
- package/src/server-fns/admin-users/create.ts +1 -1
- package/src/server-fns/admin-users/delete.ts +1 -1
- package/src/server-fns/admin-users/disable.ts +1 -1
- package/src/server-fns/admin-users/enable.ts +1 -1
- package/src/server-fns/admin-users/get-user-roles.ts +1 -1
- package/src/server-fns/admin-users/get.ts +1 -1
- package/src/server-fns/admin-users/list.ts +1 -1
- package/src/server-fns/admin-users/set-password.ts +1 -1
- package/src/server-fns/admin-users/set-user-roles.ts +1 -1
- package/src/server-fns/admin-users/update.ts +1 -1
- package/src/server-fns/ai/execute.ts +1 -2
- package/src/server-fns/auth/current-user.ts +1 -1
- package/src/server-fns/auth/sign-in.ts +1 -1
- package/src/server-fns/auth/sign-out.ts +5 -3
- package/src/server-fns/collections/audit.ts +1 -1
- package/src/server-fns/collections/copy-to-locale.ts +1 -1
- package/src/server-fns/collections/create.ts +1 -1
- package/src/server-fns/collections/delete-locale.ts +1 -1
- package/src/server-fns/collections/delete.ts +1 -1
- package/src/server-fns/collections/duplicate.ts +1 -1
- package/src/server-fns/collections/get.ts +1 -1
- package/src/server-fns/collections/history.ts +1 -1
- package/src/server-fns/collections/list.ts +1 -1
- package/src/server-fns/collections/reindex.ts +1 -1
- package/src/server-fns/collections/reorder.ts +1 -1
- package/src/server-fns/collections/restore-version.ts +1 -1
- package/src/server-fns/collections/stats.ts +2 -1
- package/src/server-fns/collections/status.ts +1 -1
- package/src/server-fns/collections/tree.ts +2 -2
- package/src/server-fns/collections/update.ts +1 -1
- package/src/server-fns/collections/upload.ts +2 -1
- package/src/server-fns/i18n/set-locale.ts +1 -1
- package/src/server-fns/preview/disable.ts +1 -1
- package/src/server-fns/preview/enable.ts +1 -2
- package/src/server-fns/preview/state.ts +1 -1
- package/dist/auth/auth-context.d.ts +0 -50
- package/dist/auth/auth-context.js +0 -61
- package/dist/auth/auth-cookies.d.ts +0 -34
- package/dist/auth/auth-cookies.js +0 -46
- package/dist/auth/preview-cookies.d.ts +0 -19
- package/dist/auth/preview-cookies.js +0 -26
- package/dist/auth/request-scope.d.ts +0 -16
- package/dist/auth/request-scope.js +0 -24
- package/dist/integrations/byline-client.d.ts +0 -24
- package/dist/integrations/byline-client.js +0 -25
- package/dist/integrations/byline-public-client.d.ts +0 -47
- package/dist/integrations/byline-public-client.js +0 -19
- package/dist/integrations/byline-viewer-client.d.ts +0 -26
- package/dist/integrations/byline-viewer-client.js +0 -44
- package/src/auth/auth-context.test.node.ts +0 -302
- package/src/auth/auth-context.ts +0 -142
- package/src/auth/auth-cookies.ts +0 -113
- package/src/auth/preview-cookies.ts +0 -73
- package/src/auth/request-scope.test.node.ts +0 -93
- package/src/auth/request-scope.ts +0 -62
- package/src/integrations/byline-client.ts +0 -68
- package/src/integrations/byline-public-client.ts +0 -67
- package/src/integrations/byline-viewer-client.ts +0 -111
- package/src/integrations/request-context-contract.test.node.ts +0 -131
- 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
|
-
})
|