@byline/host-tanstack-start 6.7.2 → 6.9.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 (74) hide show
  1. package/dist/admin-shell/admin-roles/container_module.css +1 -1
  2. package/dist/admin-shell/admin-roles/delete.js +1 -1
  3. package/dist/admin-shell/admin-users/container.js +181 -159
  4. package/dist/admin-shell/admin-users/container.module.js +2 -1
  5. package/dist/admin-shell/admin-users/container_module.css +5 -1
  6. package/dist/admin-shell/admin-users/delete.js +1 -1
  7. package/dist/admin-shell/chrome/admin-app-bar.d.ts +0 -7
  8. package/dist/admin-shell/collections/api.d.ts +6 -2
  9. package/dist/admin-shell/collections/api.js +109 -3
  10. package/dist/admin-shell/collections/api.test.d.ts +8 -0
  11. package/dist/admin-shell/collections/edit.js +2 -0
  12. package/dist/admin-shell/collections/preview-link.js +17 -13
  13. package/dist/admin-shell/collections/view-menu.d.ts +8 -1
  14. package/dist/admin-shell/collections/view-menu.js +124 -107
  15. package/dist/admin-shell/collections/view-menu.module.js +1 -0
  16. package/dist/admin-shell/collections/view-menu_module.css +15 -1
  17. package/dist/admin-shell/scheduled-publications/list.js +2 -2
  18. package/dist/admin-shell/singletons/preview-link.js +17 -13
  19. package/dist/admin-shell/singletons/view-menu.js +78 -63
  20. package/dist/admin-shell/singletons/view.js +2 -0
  21. package/dist/integrations/byline-admin-services.js +2 -0
  22. package/dist/integrations/passkey-admin-services.d.ts +9 -0
  23. package/dist/integrations/passkey-admin-services.js +53 -0
  24. package/dist/integrations/session-coordination.d.ts +3 -1
  25. package/dist/integrations/session-coordination.js +3 -1
  26. package/dist/integrations/session-middleware.d.ts +3 -1
  27. package/dist/integrations/session-middleware.js +87 -79
  28. package/dist/integrations/sign-in-body.d.ts +3 -0
  29. package/dist/integrations/sign-in-body.js +19 -4
  30. package/dist/integrations/sign-in-middleware.js +30 -5
  31. package/dist/server-fns/auth/passkey-admission.d.ts +24 -0
  32. package/dist/server-fns/auth/passkey-admission.js +88 -0
  33. package/dist/server-fns/auth/passkeys.d.ts +96 -0
  34. package/dist/server-fns/auth/passkeys.js +238 -0
  35. package/dist/server-fns/auth/reconcile-locale.d.ts +20 -0
  36. package/dist/server-fns/auth/reconcile-locale.js +16 -0
  37. package/dist/server-fns/auth/sign-in.js +1 -15
  38. package/dist/server-fns/collections/get.d.ts +1 -0
  39. package/dist/server-fns/collections/get.js +4 -0
  40. package/package.json +11 -11
  41. package/src/admin-shell/admin-roles/container.module.css +1 -1
  42. package/src/admin-shell/admin-roles/delete.tsx +1 -1
  43. package/src/admin-shell/admin-users/container.module.css +7 -1
  44. package/src/admin-shell/admin-users/container.tsx +22 -2
  45. package/src/admin-shell/admin-users/delete.tsx +1 -1
  46. package/src/admin-shell/chrome/admin-app-bar.tsx +2 -2
  47. package/src/admin-shell/chrome/dashboard.test.tsx +2 -2
  48. package/src/admin-shell/collections/api.test.tsx +223 -0
  49. package/src/admin-shell/collections/api.tsx +98 -3
  50. package/src/admin-shell/collections/edit.tsx +9 -1
  51. package/src/admin-shell/collections/preview-link.tsx +13 -12
  52. package/src/admin-shell/collections/view-menu.module.css +29 -3
  53. package/src/admin-shell/collections/view-menu.tsx +114 -96
  54. package/src/admin-shell/scheduled-publications/list.tsx +3 -4
  55. package/src/admin-shell/singletons/history.test.tsx +1 -1
  56. package/src/admin-shell/singletons/preview-link.tsx +13 -12
  57. package/src/admin-shell/singletons/view-menu.tsx +65 -59
  58. package/src/admin-shell/singletons/view.test.tsx +4 -4
  59. package/src/admin-shell/singletons/view.tsx +3 -1
  60. package/src/integrations/byline-admin-services.ts +3 -0
  61. package/src/integrations/passkey-admin-services.ts +85 -0
  62. package/src/integrations/session-client.test.node.ts +73 -0
  63. package/src/integrations/session-coordination.ts +3 -1
  64. package/src/integrations/session-middleware.ts +86 -72
  65. package/src/integrations/sign-in-body.test.node.ts +31 -1
  66. package/src/integrations/sign-in-body.ts +27 -3
  67. package/src/integrations/sign-in-middleware.ts +42 -9
  68. package/src/server-fns/auth/passkey-admission.test.node.ts +173 -0
  69. package/src/server-fns/auth/passkey-admission.ts +125 -0
  70. package/src/server-fns/auth/passkeys.test.node.ts +247 -0
  71. package/src/server-fns/auth/passkeys.ts +252 -0
  72. package/src/server-fns/auth/reconcile-locale.ts +40 -0
  73. package/src/server-fns/auth/sign-in.ts +1 -32
  74. package/src/server-fns/collections/get.ts +3 -0
@@ -6,11 +6,12 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
 
9
+ import type { ReactNode } from 'react'
9
10
  import { useEffect } from 'react'
10
11
 
11
12
  import type { CollectionAdminConfig, PreviewDocument } from '@byline/core'
12
13
  import { useTranslation } from '@byline/i18n/react'
13
- import { Button, HistoryIcon, IconButton, Label, Select } from '@byline/ui/react'
14
+ import { Button, HistoryIcon, IconButton, Label, Select, Tooltip } from '@byline/ui/react'
14
15
  import cx from 'clsx'
15
16
 
16
17
  import { getAdminRoutePath } from '../../routes/admin-path.js'
@@ -51,6 +52,7 @@ export const ViewMenu = ({
51
52
  defaultContentLocale,
52
53
  adminConfig,
53
54
  doc,
55
+ dataActions,
54
56
  }: {
55
57
  /** Collection path (e.g. "docs", "news"). */
56
58
  collection: string
@@ -78,6 +80,12 @@ export const ViewMenu = ({
78
80
  * entirely (e.g. on the create route, where no doc exists yet).
79
81
  */
80
82
  doc?: PreviewDocument
83
+ /**
84
+ * Controls that act on the data selected by the locale and depth controls
85
+ * (the API view's Copy / Download JSON). Rendered first, in the same group
86
+ * as those controls, so they wrap together ahead of the view navigation.
87
+ */
88
+ dataActions?: ReactNode
81
89
  }) => {
82
90
  const navigate = useNavigate()
83
91
  const { t } = useTranslation('byline-admin')
@@ -141,103 +149,113 @@ export const ViewMenu = ({
141
149
 
142
150
  return (
143
151
  <div className={cx('byline-view-menu', styles.root)}>
144
- <Label
145
- className={cx('muted byline-view-menu-label', styles.label)}
146
- id="contentLocaleLabel"
147
- htmlFor="contentLocale"
148
- label={t('collections.viewMenu.contentLocaleLabel')}
149
- />
150
- <Select<string>
151
- name="contentLocale"
152
- id="contentLocale"
153
- className={cx('byline-view-menu-locale-select', styles.localeSelect)}
154
- size="xs"
155
- variant="outlined"
156
- value={locale ?? defaultContentLocale}
157
- items={[
158
- ...(activeView !== 'edit'
159
- ? [{ value: 'all', label: t('collections.viewMenu.localeAll') }]
160
- : []),
161
- ...contentLocales.map((loc) => ({ value: loc.code, label: loc.label })),
162
- ]}
163
- onValueChange={handleLocaleChange}
164
- />
165
- {activeView === 'api' && (
166
- <>
167
- <Label
168
- className={cx('muted byline-view-menu-label', styles.label)}
169
- id="populateDepthLabel"
170
- htmlFor="populateDepth"
171
- label={t('collections.viewMenu.depthLabel')}
152
+ <div className={cx('byline-view-menu-data', styles.group)}>
153
+ {dataActions}
154
+ <Label
155
+ className={cx('muted byline-view-menu-label', styles.label)}
156
+ id="contentLocaleLabel"
157
+ htmlFor="contentLocale"
158
+ label={t('collections.viewMenu.contentLocaleLabel')}
159
+ />
160
+ <Select<string>
161
+ name="contentLocale"
162
+ id="contentLocale"
163
+ className={cx('byline-view-menu-locale-select', styles.localeSelect)}
164
+ size="xs"
165
+ variant="outlined"
166
+ value={locale ?? defaultContentLocale}
167
+ items={[
168
+ ...(activeView !== 'edit'
169
+ ? [{ value: 'all', label: t('collections.viewMenu.localeAll') }]
170
+ : []),
171
+ ...contentLocales.map((loc) => ({ value: loc.code, label: loc.label })),
172
+ ]}
173
+ onValueChange={handleLocaleChange}
174
+ />
175
+ {activeView === 'api' && (
176
+ <>
177
+ <Label
178
+ className={cx('muted byline-view-menu-label', styles.label)}
179
+ id="populateDepthLabel"
180
+ htmlFor="populateDepth"
181
+ label={t('collections.viewMenu.depthLabel')}
182
+ />
183
+ <Select<string>
184
+ name="populateDepth"
185
+ id="populateDepth"
186
+ className={cx('byline-view-menu-depth-select', styles.depthSelect)}
187
+ size="xs"
188
+ variant="outlined"
189
+ value={String(depth ?? 0)}
190
+ items={[
191
+ { value: '0', label: '0' },
192
+ { value: '1', label: '1' },
193
+ { value: '2', label: '2' },
194
+ { value: '3', label: '3' },
195
+ ]}
196
+ onValueChange={handleDepthChange}
197
+ />
198
+ </>
199
+ )}
200
+ </div>
201
+ <div className={cx('byline-view-menu-nav', styles.group)}>
202
+ {doc && (
203
+ <PreviewLink
204
+ collectionPath={collection}
205
+ doc={doc}
206
+ adminConfig={adminConfig}
207
+ locale={locale}
208
+ className={cx('byline-view-menu-icon-button', styles.iconButton)}
172
209
  />
173
- <Select<string>
174
- name="populateDepth"
175
- id="populateDepth"
176
- className={cx('byline-view-menu-depth-select', styles.depthSelect)}
210
+ )}
211
+ <Tooltip text={t('collections.breadcrumbs.history')} side="bottom" delay={0}>
212
+ <IconButton
213
+ className={cx('byline-view-menu-icon-button', styles.iconButton)}
177
214
  size="xs"
178
- variant="outlined"
179
- value={String(depth ?? 0)}
180
- items={[
181
- { value: '0', label: '0' },
182
- { value: '1', label: '1' },
183
- { value: '2', label: '2' },
184
- { value: '3', label: '3' },
185
- ]}
186
- onValueChange={handleDepthChange}
187
- />
188
- </>
189
- )}
190
- {doc && (
191
- <PreviewLink
192
- collectionPath={collection}
193
- doc={doc}
194
- adminConfig={adminConfig}
195
- locale={locale}
196
- className={cx('byline-view-menu-icon-button', styles.iconButton)}
197
- />
198
- )}
199
- <IconButton
200
- className={cx('byline-view-menu-icon-button', styles.iconButton)}
201
- size="xs"
202
- variant={activeView === 'history' ? 'filled' : 'text'}
203
- onClick={() =>
204
- navigate({
205
- to: getAdminRoutePath('collections', '$collection', '$id', 'history'),
206
- params: { collection, id: documentId },
207
- search: locale ? { locale } : {},
208
- })
209
- }
210
- >
211
- <HistoryIcon className={cx('byline-view-menu-icon-button-icon', styles.iconButtonIcon)} />
212
- </IconButton>
213
- <Button
214
- size="xs"
215
- variant={activeView === 'edit' ? 'filled' : 'outlined'}
216
- className={cx('byline-view-menu-button', styles.button)}
217
- onClick={() =>
218
- navigate({
219
- to: getAdminRoutePath('collections', '$collection', '$id'),
220
- params: { collection, id: documentId },
221
- search: locale ? { locale } : {},
222
- })
223
- }
224
- >
225
- {t('common.actions.edit')}
226
- </Button>
227
- <Button
228
- size="xs"
229
- variant={activeView === 'api' ? 'filled' : 'outlined'}
230
- className={cx('byline-view-menu-button', styles.button)}
231
- onClick={() =>
232
- navigate({
233
- to: getAdminRoutePath('collections', '$collection', '$id', 'api'),
234
- params: { collection, id: documentId },
235
- search: locale ? { locale } : {},
236
- })
237
- }
238
- >
239
- {t('collections.viewMenu.apiButton')}
240
- </Button>
215
+ variant={activeView === 'history' ? 'filled' : 'text'}
216
+ aria-label={t('collections.breadcrumbs.history')}
217
+ onClick={() =>
218
+ navigate({
219
+ to: getAdminRoutePath('collections', '$collection', '$id', 'history'),
220
+ params: { collection, id: documentId },
221
+ search: locale ? { locale } : {},
222
+ })
223
+ }
224
+ >
225
+ <HistoryIcon
226
+ className={cx('byline-view-menu-icon-button-icon', styles.iconButtonIcon)}
227
+ />
228
+ </IconButton>
229
+ </Tooltip>
230
+ <Button
231
+ size="xs"
232
+ variant={activeView === 'edit' ? 'filled' : 'outlined'}
233
+ className={cx('byline-view-menu-button', styles.button)}
234
+ onClick={() =>
235
+ navigate({
236
+ to: getAdminRoutePath('collections', '$collection', '$id'),
237
+ params: { collection, id: documentId },
238
+ search: locale ? { locale } : {},
239
+ })
240
+ }
241
+ >
242
+ {t('common.actions.edit')}
243
+ </Button>
244
+ <Button
245
+ size="xs"
246
+ variant={activeView === 'api' ? 'filled' : 'outlined'}
247
+ className={cx('byline-view-menu-button', styles.button)}
248
+ onClick={() =>
249
+ navigate({
250
+ to: getAdminRoutePath('collections', '$collection', '$id', 'api'),
251
+ params: { collection, id: documentId },
252
+ search: locale ? { locale } : {},
253
+ })
254
+ }
255
+ >
256
+ {t('collections.viewMenu.apiButton')}
257
+ </Button>
258
+ </div>
241
259
  </div>
242
260
  )
243
261
  }
@@ -1,7 +1,4 @@
1
- import { DocumentMutationNotice } from '../document-mutation-notice.js'
2
- import { useDocumentMutationState } from '../document-mutation-state.js'
3
-
4
- ;('use client')
1
+ 'use client'
5
2
 
6
3
  /**
7
4
  * This Source Code is subject to the terms of the Mozilla Public
@@ -32,6 +29,8 @@ import { getAdminRoutePath } from '../../routes/admin-path.js'
32
29
  import { cancelCollectionDocumentScheduledPublish } from '../../server-fns/collections/index.js'
33
30
  import { Link, useNavigate } from '../chrome/loose-router.js'
34
31
  import { RouterPager } from '../chrome/router-pager.js'
32
+ import { DocumentMutationNotice } from '../document-mutation-notice.js'
33
+ import { useDocumentMutationState } from '../document-mutation-state.js'
35
34
  import styles from './list.module.css'
36
35
  import type { ScheduledPublicationListResponse } from '../../server-fns/collections/index.js'
37
36
 
@@ -372,7 +372,7 @@ describe('singleton version history', () => {
372
372
  <HistoryView
373
373
  collectionDefinition={collectionDefinition}
374
374
  adminConfig={{
375
- slug: 'articles',
375
+ path: 'articles',
376
376
  columns: [{ fieldName: 'title', label: 'Title' }],
377
377
  }}
378
378
  data={data as never}
@@ -12,7 +12,7 @@ import { useState } from 'react'
12
12
 
13
13
  import type { SingletonAdminConfig, SingletonPreviewDocument } from '@byline/core'
14
14
  import { useTranslation } from '@byline/i18n/react'
15
- import { ExternalLinkIcon, IconButton, useToastManager } from '@byline/ui/react'
15
+ import { ExternalLinkIcon, IconButton, Tooltip, useToastManager } from '@byline/ui/react'
16
16
  import cx from 'clsx'
17
17
 
18
18
  import { enablePreviewModeFn } from '../../server-fns/preview/index.js'
@@ -60,16 +60,17 @@ export function SingletonPreviewLink({
60
60
  }
61
61
 
62
62
  return (
63
- <IconButton
64
- className={cx('byline-preview-link', className)}
65
- size="xs"
66
- variant="text"
67
- disabled={busy}
68
- onClick={handleClick}
69
- aria-label={t('collections.preview.openAriaLabel')}
70
- title={t('collections.preview.title')}
71
- >
72
- <ExternalLinkIcon width="20px" height="20px" className="byline-preview-link-icon" />
73
- </IconButton>
63
+ <Tooltip text={t('collections.preview.title')} side="bottom" delay={0}>
64
+ <IconButton
65
+ className={cx('byline-preview-link', className)}
66
+ size="xs"
67
+ variant="text"
68
+ disabled={busy}
69
+ onClick={handleClick}
70
+ aria-label={t('collections.preview.openAriaLabel')}
71
+ >
72
+ <ExternalLinkIcon width="20px" height="20px" className="byline-preview-link-icon" />
73
+ </IconButton>
74
+ </Tooltip>
74
75
  )
75
76
  }
@@ -12,7 +12,7 @@ import { useEffect } from 'react'
12
12
 
13
13
  import type { SingletonAdminConfig, SingletonPreviewDocument } from '@byline/core'
14
14
  import { useTranslation } from '@byline/i18n/react'
15
- import { Button, HistoryIcon, IconButton, Label, Select } from '@byline/ui/react'
15
+ import { Button, HistoryIcon, IconButton, Label, Select, Tooltip } from '@byline/ui/react'
16
16
  import cx from 'clsx'
17
17
 
18
18
  import { getAdminRoutePath } from '../../routes/admin-path.js'
@@ -71,66 +71,72 @@ export function SingletonViewMenu({
71
71
 
72
72
  return (
73
73
  <div className={cx('byline-view-menu', collectionStyles.root)}>
74
- <Label
75
- className={cx('muted byline-view-menu-label', collectionStyles.label)}
76
- id="contentLocaleLabel"
77
- htmlFor="contentLocale"
78
- label={t('collections.viewMenu.contentLocaleLabel')}
79
- />
80
- <Select<string>
81
- name="contentLocale"
82
- id="contentLocale"
83
- className={cx('byline-view-menu-locale-select', collectionStyles.localeSelect)}
84
- size="xs"
85
- variant="outlined"
86
- value={locale ?? defaultContentLocale}
87
- items={[
88
- ...(activeView === 'history'
89
- ? [{ value: 'all', label: t('collections.viewMenu.localeAll') }]
90
- : []),
91
- ...contentLocales.map((entry) => ({ value: entry.code, label: entry.label })),
92
- ]}
93
- onValueChange={handleLocaleChange}
94
- />
95
- {adminConfig?.preview != null && doc != null && (
96
- <SingletonPreviewLink
97
- doc={doc}
98
- adminConfig={adminConfig}
99
- locale={locale}
100
- className={cx('byline-view-menu-icon-button', collectionStyles.iconButton)}
74
+ <div className={cx('byline-view-menu-data', collectionStyles.group)}>
75
+ <Label
76
+ className={cx('muted byline-view-menu-label', collectionStyles.label)}
77
+ id="contentLocaleLabel"
78
+ htmlFor="contentLocale"
79
+ label={t('collections.viewMenu.contentLocaleLabel')}
101
80
  />
102
- )}
103
- <IconButton
104
- className={cx('byline-view-menu-icon-button', collectionStyles.iconButton)}
105
- size="xs"
106
- variant={activeView === 'history' ? 'filled' : 'text'}
107
- aria-label={t('collections.breadcrumbs.history')}
108
- onClick={() =>
109
- navigate({
110
- to: getAdminRoutePath('singletons', '$singleton', 'history'),
111
- params: { singleton },
112
- search: locale ? { locale } : {},
113
- })
114
- }
115
- >
116
- <HistoryIcon
117
- className={cx('byline-view-menu-icon-button-icon', collectionStyles.iconButtonIcon)}
81
+ <Select<string>
82
+ name="contentLocale"
83
+ id="contentLocale"
84
+ className={cx('byline-view-menu-locale-select', collectionStyles.localeSelect)}
85
+ size="xs"
86
+ variant="outlined"
87
+ value={locale ?? defaultContentLocale}
88
+ items={[
89
+ ...(activeView === 'history'
90
+ ? [{ value: 'all', label: t('collections.viewMenu.localeAll') }]
91
+ : []),
92
+ ...contentLocales.map((entry) => ({ value: entry.code, label: entry.label })),
93
+ ]}
94
+ onValueChange={handleLocaleChange}
118
95
  />
119
- </IconButton>
120
- <Button
121
- size="xs"
122
- variant={activeView === 'edit' ? 'filled' : 'outlined'}
123
- className={cx('byline-view-menu-button', collectionStyles.button)}
124
- onClick={() =>
125
- navigate({
126
- to: getAdminRoutePath('singletons', '$singleton'),
127
- params: { singleton },
128
- search: locale ? { locale } : {},
129
- })
130
- }
131
- >
132
- {t('common.actions.edit')}
133
- </Button>
96
+ </div>
97
+ <div className={cx('byline-view-menu-nav', collectionStyles.group)}>
98
+ {adminConfig?.preview != null && doc != null && (
99
+ <SingletonPreviewLink
100
+ doc={doc}
101
+ adminConfig={adminConfig}
102
+ locale={locale}
103
+ className={cx('byline-view-menu-icon-button', collectionStyles.iconButton)}
104
+ />
105
+ )}
106
+ <Tooltip text={t('collections.breadcrumbs.history')} side="bottom" delay={0}>
107
+ <IconButton
108
+ className={cx('byline-view-menu-icon-button', collectionStyles.iconButton)}
109
+ size="xs"
110
+ variant={activeView === 'history' ? 'filled' : 'text'}
111
+ aria-label={t('collections.breadcrumbs.history')}
112
+ onClick={() =>
113
+ navigate({
114
+ to: getAdminRoutePath('singletons', '$singleton', 'history'),
115
+ params: { singleton },
116
+ search: locale ? { locale } : {},
117
+ })
118
+ }
119
+ >
120
+ <HistoryIcon
121
+ className={cx('byline-view-menu-icon-button-icon', collectionStyles.iconButtonIcon)}
122
+ />
123
+ </IconButton>
124
+ </Tooltip>
125
+ <Button
126
+ size="xs"
127
+ variant={activeView === 'edit' ? 'filled' : 'outlined'}
128
+ className={cx('byline-view-menu-button', collectionStyles.button)}
129
+ onClick={() =>
130
+ navigate({
131
+ to: getAdminRoutePath('singletons', '$singleton'),
132
+ params: { singleton },
133
+ search: locale ? { locale } : {},
134
+ })
135
+ }
136
+ >
137
+ {t('common.actions.edit')}
138
+ </Button>
139
+ </div>
134
140
  </div>
135
141
  )
136
142
  }
@@ -142,7 +142,7 @@ const singleton = defineSingleton({
142
142
 
143
143
  const adminConfig = {
144
144
  singleton: true as const,
145
- slug: singleton.path,
145
+ path: singleton.path,
146
146
  preview: { url: () => '/preview-settings' },
147
147
  }
148
148
 
@@ -181,7 +181,7 @@ let root: Root
181
181
  function view(
182
182
  currentDocument: typeof loadedDocument | null,
183
183
  locale = 'en',
184
- config: typeof adminConfig | { singleton: true; slug: string } = adminConfig
184
+ config: typeof adminConfig | { singleton: true; path: string } = adminConfig
185
185
  ) {
186
186
  return (
187
187
  <I18nProvider
@@ -208,7 +208,7 @@ function view(
208
208
  function render(
209
209
  currentDocument: typeof loadedDocument | null,
210
210
  locale = 'en',
211
- config: typeof adminConfig | { singleton: true; slug: string } = adminConfig
211
+ config: typeof adminConfig | { singleton: true; path: string } = adminConfig
212
212
  ) {
213
213
  act(() => root.render(view(currentDocument, locale, config)))
214
214
  }
@@ -332,7 +332,7 @@ describe('SingletonView', () => {
332
332
  })
333
333
 
334
334
  it('never derives a singleton preview from its internal stored path', () => {
335
- render(loadedDocument, 'en', { singleton: true, slug: singleton.path })
335
+ render(loadedDocument, 'en', { singleton: true, path: singleton.path })
336
336
 
337
337
  expect(container.querySelector('.byline-view-menu')).not.toBeNull()
338
338
  expect(container.querySelector('.byline-preview-link')).toBeNull()
@@ -11,7 +11,7 @@
11
11
  import { useRouter } from '@tanstack/react-router'
12
12
 
13
13
  import { FormRenderer } from '@byline/admin/react'
14
- import type { SingletonAdminConfig, SingletonDefinition } from '@byline/core'
14
+ import type { SingletonAdminConfig, SingletonDefinition, UnavailableBlock } from '@byline/core'
15
15
  import {
16
16
  getDefaultStatus,
17
17
  getWorkflow,
@@ -82,6 +82,7 @@ export function SingletonView({
82
82
  ? workflowStatuses[currentIndex + 1]
83
83
  : undefined
84
84
  const publishedVersion = document?._publishedVersion ?? null
85
+ const unavailableBlocks = document?._unavailableBlocks as UnavailableBlock[] | undefined
85
86
  const restoreWarnings = document?._restoreWarnings as string[] | undefined
86
87
  const scheduledPublicationEnabled = document?._scheduledPublicationEnabled === true
87
88
  const canSchedulePublication = document?._canSchedulePublication === true
@@ -441,6 +442,7 @@ export function SingletonView({
441
442
  contentLocales={document == null ? undefined : contentLocales}
442
443
  publishedVersion={document == null ? null : publishedVersion}
443
444
  restoreWarnings={restoreWarnings}
445
+ unavailableBlocks={unavailableBlocks}
444
446
  nextStatus={document == null ? undefined : nextStatus}
445
447
  workflowStatuses={document == null ? undefined : workflowStatuses}
446
448
  onCancel={() => navigate({ to: getAdminRoutePath() })}
@@ -36,6 +36,7 @@ import {
36
36
  import { adminSignIn } from '../server-fns/auth/index.js'
37
37
  import { getCollectionDocumentVersion as serverGetCollectionDocumentVersion } from '../server-fns/collections/get.js'
38
38
  import { setAdminLocaleFn } from '../server-fns/i18n/index.js'
39
+ import { passkeyAdminServices } from './passkey-admin-services.js'
39
40
  import { acceptSession, coordinateAuthAction, notifySessionAction } from './session-coordination.js'
40
41
 
41
42
  /**
@@ -70,6 +71,8 @@ export const bylineAdminServices: BylineAdminServices = {
70
71
  return result
71
72
  },
72
73
 
74
+ passkeys: passkeyAdminServices,
75
+
73
76
  // Account self-service
74
77
  updateAccount,
75
78
  changeAccountPassword,
@@ -0,0 +1,85 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import type { PasskeyAdminServices } from '@byline/admin/services'
10
+
11
+ import {
12
+ getPasskeyCapability,
13
+ passkeyEnrollment,
14
+ passkeyEnrollmentOptions,
15
+ passkeyList,
16
+ passkeyManagementContext,
17
+ passkeyPasswordProof,
18
+ passkeyProof,
19
+ passkeyProofOptions,
20
+ passkeyRemove,
21
+ passkeyRename,
22
+ passkeySignIn,
23
+ passkeySignInOptions,
24
+ } from '../server-fns/auth/passkeys.js'
25
+ import {
26
+ acceptSession,
27
+ coordinateAuthAction,
28
+ flagSessionChanged,
29
+ notifySessionAction,
30
+ requireUnchanged,
31
+ sessionChangedError,
32
+ sessionSnapshot,
33
+ } from './session-coordination.js'
34
+
35
+ export const passkeyAdminServices: PasskeyAdminServices = {
36
+ capability: () => getPasskeyCapability(),
37
+ captureLogin: (signIn = false) => {
38
+ const snapshot = sessionSnapshot()
39
+ const assertCurrent = () => {
40
+ if (!signIn) return requireUnchanged(snapshot)
41
+ // Explicit reauthentication may begin on an already flagged page, but any
42
+ // subsequent account change still invalidates this particular ceremony.
43
+ const current = sessionSnapshot()
44
+ if (current.epoch !== snapshot.epoch || current.sessionId !== snapshot.sessionId)
45
+ throw sessionChangedError()
46
+ }
47
+ assertCurrent()
48
+ return { expectedSessionId: snapshot.sessionId, assertCurrent }
49
+ },
50
+ managementContext: async (binding) => {
51
+ binding.assertCurrent()
52
+ if (!binding.expectedSessionId) throw sessionChangedError()
53
+ const actor = await passkeyManagementContext({
54
+ data: { expectedSessionId: binding.expectedSessionId },
55
+ })
56
+ binding.assertCurrent()
57
+ return actor
58
+ },
59
+ signInOptions: passkeySignInOptions,
60
+ signIn: (args, binding) =>
61
+ coordinateAuthAction(async () => {
62
+ binding.assertCurrent()
63
+ const result = await passkeySignIn(args)
64
+ acceptSession(result.sessionId)
65
+ notifySessionAction()
66
+ return result
67
+ }),
68
+ passwordProof: passkeyPasswordProof,
69
+ proofOptions: passkeyProofOptions,
70
+ proof: passkeyProof,
71
+ enrollmentOptions: passkeyEnrollmentOptions,
72
+ enrollment: passkeyEnrollment,
73
+ list: passkeyList,
74
+ rename: passkeyRename,
75
+ remove: async (args) => {
76
+ // The mutation middleware queues its fetch, including cookie application;
77
+ // it releases the queue before a pre-handler renewal can acquire it.
78
+ const result = await passkeyRemove(args)
79
+ if (result.signedOut) {
80
+ flagSessionChanged()
81
+ notifySessionAction()
82
+ }
83
+ return result
84
+ },
85
+ }