@swiss-ai-hub/web 0.319.0 → 0.321.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 (50) hide show
  1. package/{pages/auth/login.vue → components/Auth/LoginPanel.vue} +3 -33
  2. package/components/FormKit/AgentSelector.vue +20 -7
  3. package/components/FormKit/CronInput.vue +208 -0
  4. package/components/FormKit/Repeater.vue +1 -4
  5. package/components/FormKit/VectorStoreInput.vue +43 -6
  6. package/components/Knowledge/Database/CreateModal.vue +315 -0
  7. package/components/Knowledge/Database/EmptyCard.vue +34 -0
  8. package/components/Knowledge/DeleteConfirmModal.vue +87 -0
  9. package/components/Knowledge/Document/List.vue +20 -9
  10. package/components/Knowledge/Namespace/Card.vue +18 -0
  11. package/components/Knowledge/Namespace/CreateModal.vue +11 -3
  12. package/components/Role/AccessCapabilities.vue +4 -4
  13. package/components/Role/AccessCapabilityGroup.vue +22 -6
  14. package/components/Role/AccessRulesEditor.vue +15 -12
  15. package/composables/app/useAppVersion.ts +37 -20
  16. package/composables/auth/useAuth.ts +3 -11
  17. package/composables/document/useCreateDatabase.ts +21 -0
  18. package/composables/document/useDeleteDatabase.ts +29 -0
  19. package/composables/document/useDeleteDocument.ts +1 -1
  20. package/composables/document/useDeleteDocuments.ts +1 -1
  21. package/composables/document/useDeleteNamespace.ts +30 -0
  22. package/composables/document/useIngestors.ts +23 -0
  23. package/composables/document/useScheduledDeletions.ts +16 -8
  24. package/composables/form/useCreateInstanceForm.ts +2 -2
  25. package/composables/form/useFormKitTransform.ts +44 -15
  26. package/formkit.config.ts +57 -3
  27. package/i18n/locales/de.yaml +84 -5
  28. package/i18n/locales/en.yaml +80 -3
  29. package/i18n/locales/fr.yaml +82 -3
  30. package/i18n/locales/it.yaml +84 -3
  31. package/middleware/agent-admin.ts +31 -0
  32. package/middleware/auth.global.ts +34 -16
  33. package/package.json +1 -1
  34. package/pages/[tenant]/service/agents.vue +3 -0
  35. package/pages/[tenant]/service/knowledge/[db]/[namespace]/[document_id].vue +12 -5
  36. package/pages/[tenant]/service/knowledge.vue +138 -0
  37. package/pages/auth/login/[idp].vue +57 -0
  38. package/pages/auth/login/index.vue +46 -0
  39. package/plugins/0.runtime-config.client.ts +5 -0
  40. package/plugins/oidc-client.ts +1 -1
  41. package/sdk/client/client/client.gen.ts +1 -3
  42. package/sdk/client/client/types.gen.ts +7 -13
  43. package/sdk/client/client/utils.gen.ts +1 -2
  44. package/sdk/client/core/queryKeySerializer.gen.ts +1 -6
  45. package/sdk/client/core/types.gen.ts +5 -3
  46. package/sdk/client/index.ts +47 -4
  47. package/sdk/client/schemas.gen.ts +1787 -780
  48. package/sdk/client/sdk.gen.ts +231 -1
  49. package/sdk/client/types.gen.ts +1103 -226
  50. package/plugins/keycloak-client.ts +0 -41
@@ -6,20 +6,42 @@ let lastSyncedTenant: string | null = null
6
6
 
7
7
  const REDIRECT_KEY = 'aihub_redirect_after_login'
8
8
 
9
+ const normalize = (path: string) => (path.endsWith('/') ? path.slice(0, -1) : path)
10
+
11
+ const stripLocale = (path: string, localeCodes: string[]): string => {
12
+ const prefix = localeCodes.find(code => path === `/${code}` || path.startsWith(`/${code}/`))
13
+ return prefix ? path.slice(prefix.length + 1) : path
14
+ }
15
+
16
+ /**
17
+ * The whole login subtree is anonymous, not just the login page itself:
18
+ * per-tenant login links live at `/auth/login/<idp-alias>`. No
19
+ * authenticated-only page may ever be nested below that path.
20
+ *
21
+ * The locale prefix is optional. This middleware runs before @nuxtjs/i18n's
22
+ * `locale-changing` middleware (Nuxt orders file-based global middleware ahead
23
+ * of plugin-registered ones), so bouncing a hand-distributed link that dropped
24
+ * `/en/` would strip the tenant before i18n ever restores the prefix.
25
+ */
26
+ const isAnonymousPath = (path: string, localeCodes: string[]): boolean => {
27
+ const unprefixedPath = stripLocale(normalize(path), localeCodes)
28
+
29
+ return ['/auth/login', '/auth/callback', '/auth/renew'].includes(unprefixedPath)
30
+ || unprefixedPath.startsWith('/auth/login/')
31
+ }
32
+
33
+ const rememberRedirect = (fullPath: string, isAuthPath: boolean) => {
34
+ if (import.meta.client && fullPath !== '/' && !isAuthPath) {
35
+ sessionStorage.setItem(REDIRECT_KEY, fullPath)
36
+ }
37
+ }
38
+
9
39
  export default defineNuxtRouteMiddleware(async (to) => {
10
40
  const { $auth, $i18n } = useNuxtApp()
11
41
  const locale = $i18n.locale.value
42
+ const localeCodes = $i18n.locales.value.map(entry => entry.code)
12
43
 
13
- const noAuthPaths = [
14
- `/${locale}/auth/login`,
15
- `/${locale}/auth/callback`,
16
- `/${locale}/auth/renew`,
17
- ]
18
-
19
- // No auth check for public paths (normalize trailing slashes on both sides)
20
- const normalize = (p: string) => (p.endsWith('/') ? p.slice(0, -1) : p)
21
- const normalizedPath = normalize(to.path)
22
- if (noAuthPaths.some(p => normalize(p) === normalizedPath)) {
44
+ if (isAnonymousPath(to.path, localeCodes)) {
23
45
  return
24
46
  }
25
47
 
@@ -29,9 +51,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
29
51
  const isAuthPath = to.path.includes('/auth/')
30
52
 
31
53
  if (!user) {
32
- if (import.meta.client && to.fullPath !== '/' && !isAuthPath) {
33
- sessionStorage.setItem(REDIRECT_KEY, to.fullPath)
34
- }
54
+ rememberRedirect(to.fullPath, isAuthPath)
35
55
  return navigateTo(`/${locale}/auth/login`)
36
56
  }
37
57
 
@@ -41,9 +61,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
41
61
  }
42
62
  catch {
43
63
  await $auth.removeUser()
44
- if (import.meta.client && to.fullPath !== '/' && !isAuthPath) {
45
- sessionStorage.setItem(REDIRECT_KEY, to.fullPath)
46
- }
64
+ rememberRedirect(to.fullPath, isAuthPath)
47
65
  return navigateTo(`/${locale}/auth/login`)
48
66
  }
49
67
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "license": "AGPL-3.0-or-later",
4
4
  "author": "bbv Software Services AG (https://www.bbv.ch)",
5
5
  "type": "module",
6
- "version": "0.319.0",
6
+ "version": "0.321.0",
7
7
  "description": "Swiss AI Hub - Admin & Management UI (Nuxt 3 layer)",
8
8
  "main": "./nuxt.config.ts",
9
9
  "repository": {
@@ -138,6 +138,9 @@ import { AgentConfigImportError } from '@core/composables/agent/useImportAgentIn
138
138
 
139
139
  import type { FullAgentInstanceDto, WorkflowGraph } from '@core/sdk/client'
140
140
 
141
+ // Nuxt collects middleware from every matched record, so this guards the whole /service/agents subtree.
142
+ definePageMeta({ middleware: 'agent-admin' })
143
+
141
144
  type AgentGroup = {
142
145
  agentClass: string
143
146
  name: string
@@ -82,20 +82,27 @@ const confirmDelete = () => {
82
82
  }
83
83
 
84
84
  const handleDelete = async () => {
85
+ const database = route.params.db as string
86
+ const namespace = route.params.namespace as string
87
+ const documentId = route.params.document_id as string
88
+
85
89
  try {
90
+ // Leave before deleting: the delete invalidates a key that is a prefix of this document's own query, so a
91
+ // still-mounted page refetches the document it just removed.
92
+ await router.push(tenantPath(`/service/knowledge/${database}/${namespace}`))
93
+
86
94
  await deleteDocument({
87
95
  tenantId: tenantId.value!,
88
- database: route.params.db as string,
89
- namespace: route.params.namespace as string,
90
- documentId: route.params.document_id as string,
96
+ database,
97
+ namespace,
98
+ documentId,
91
99
  })
92
- schedule([route.params.document_id as string])
100
+ schedule([documentId])
93
101
  toast.add({
94
102
  severity: 'success',
95
103
  summary: t('document.delete.success'),
96
104
  life: 3000,
97
105
  })
98
- router.push(tenantPath(`/service/knowledge/${route.params.db}/${route.params.namespace}`))
99
106
  }
100
107
  catch (error) {
101
108
  toast.add({
@@ -5,6 +5,7 @@
5
5
  :loading="databasesAreLoading"
6
6
  >
7
7
  <div class="flex flex-col gap-12">
8
+ <KnowledgeDatabaseEmptyCard @add="openNewDatabaseModal" />
8
9
  <div
9
10
  v-for="database in databases"
10
11
  :key="database.name"
@@ -21,6 +22,19 @@
21
22
  class="pi pi-lock-open text-surface-400 dark:text-surface-500"
22
23
  :title="t('knowledge.manual_management.description')"
23
24
  />
25
+ <span class="text-xs text-surface-500 dark:text-surface-400">
26
+ {{ t('knowledge.pipeline', { name: capitalCase(database.ingestor) }) }}
27
+ </span>
28
+ <Button
29
+ v-if="database.deletable"
30
+ v-tooltip.top="t('knowledge.delete_database')"
31
+ icon="pi pi-trash"
32
+ rounded
33
+ text
34
+ size="small"
35
+ severity="danger"
36
+ @click="openDeleteDatabaseModal(database)"
37
+ />
24
38
  </div>
25
39
  <div class="grid grid-cols-2 gap-4 2xl:grid-cols-2">
26
40
  <KnowledgeNamespaceCard
@@ -31,6 +45,7 @@
31
45
  @click="toNamespace(database.name, namespace)"
32
46
  @upload="openUploadModal(database, namespace)"
33
47
  @edit="openEditNamespaceModal(namespace)"
48
+ @delete="openDeleteNamespaceModal(database, namespace)"
34
49
  />
35
50
  <KnowledgeNamespaceEmptyCard
36
51
  v-if="!database.auto_sync"
@@ -63,6 +78,20 @@
63
78
  :namespace="editingNamespace"
64
79
  @success="handleUpdateSuccess"
65
80
  />
81
+
82
+ <KnowledgeDatabaseCreateModal
83
+ v-model="newDatabaseModalVisible"
84
+ @success="handleDatabaseCreationSuccess"
85
+ />
86
+
87
+ <KnowledgeDeleteConfirmModal
88
+ v-model:visible="deleteModalVisible"
89
+ :title="deleteTitle"
90
+ :warning="deleteWarning"
91
+ :expected-name="deleteExpectedName"
92
+ :is-deleting="isDeleting"
93
+ @confirm="handleConfirmDelete"
94
+ />
66
95
  </StructuralScreen>
67
96
  </template>
68
97
 
@@ -71,12 +100,18 @@ import { capitalCase } from 'change-case'
71
100
 
72
101
  import type { DatabaseDto, NamespaceDto } from '@core/sdk/client'
73
102
 
103
+ const route = useRoute()
74
104
  const router = useRouter()
75
105
  const tenantPath = useTenantPath()
76
106
  const { t } = useI18n()
107
+ const toast = useToast()
108
+ const { tenantId } = useTenant()
77
109
 
78
110
  const { databases, databasesAreLoading } = useDatabases()
79
111
 
112
+ const { deleteDatabase, isDeleting: isDeletingDatabase } = useDeleteDatabase()
113
+ const { deleteNamespace, isDeleting: isDeletingNamespace } = useDeleteNamespace()
114
+
80
115
  const uploadModalVisible = ref(false)
81
116
  const selectedDatabaseForUpload = ref('')
82
117
  const selectedNamespaceForUpload = ref('')
@@ -89,6 +124,8 @@ const selectedDatabaseForNewNamespace = ref('')
89
124
  const editNamespaceModalVisible = ref(false)
90
125
  const editingNamespace = ref<NamespaceDto | null>(null)
91
126
 
127
+ const newDatabaseModalVisible = ref(false)
128
+
92
129
  const toNamespace = (database_name: string, namespace: NamespaceDto) => {
93
130
  router.push(tenantPath(`/service/knowledge/${database_name}/${namespace.name}`))
94
131
  }
@@ -123,4 +160,105 @@ const openEditNamespaceModal = (namespace: NamespaceDto) => {
123
160
  const handleUpdateSuccess = () => {
124
161
  editingNamespace.value = null
125
162
  }
163
+
164
+ const openNewDatabaseModal = () => {
165
+ newDatabaseModalVisible.value = true
166
+ }
167
+
168
+ const handleDatabaseCreationSuccess = () => {
169
+ newDatabaseModalVisible.value = false
170
+ }
171
+
172
+ type PendingDeletion
173
+ = | { type: 'database', database: string, name: string, count: number }
174
+ | { type: 'namespace', database: string, namespace: string, name: string, count: number }
175
+
176
+ const deleteModalVisible = ref(false)
177
+ const pendingDeletion = ref<PendingDeletion | null>(null)
178
+
179
+ const isDeleting = computed(() => isDeletingDatabase.value || isDeletingNamespace.value)
180
+
181
+ const deleteExpectedName = computed(() => pendingDeletion.value?.name ?? '')
182
+
183
+ const deleteTitle = computed(() =>
184
+ pendingDeletion.value?.type === 'database'
185
+ ? t('knowledge.delete.database.title')
186
+ : t('knowledge.delete.namespace.title'),
187
+ )
188
+
189
+ const deleteWarning = computed(() => {
190
+ const pending = pendingDeletion.value
191
+ if (!pending) return ''
192
+ const params = { name: pending.name, count: pending.count }
193
+ return pending.type === 'database'
194
+ ? t('knowledge.delete.database.warning', params)
195
+ : t('knowledge.delete.namespace.warning', params)
196
+ })
197
+
198
+ const documentCountFor = (database: DatabaseDto) =>
199
+ (database.namespaces ?? []).reduce((sum, namespace) => sum + (namespace.number_of_documents ?? 0), 0)
200
+
201
+ const openDeleteDatabaseModal = (database: DatabaseDto) => {
202
+ pendingDeletion.value = {
203
+ type: 'database',
204
+ database: database.name,
205
+ name: database.name,
206
+ count: documentCountFor(database),
207
+ }
208
+ deleteModalVisible.value = true
209
+ }
210
+
211
+ const openDeleteNamespaceModal = (database: DatabaseDto, namespace: NamespaceDto) => {
212
+ pendingDeletion.value = {
213
+ type: 'namespace',
214
+ database: database.name,
215
+ namespace: namespace.name,
216
+ name: namespace.name,
217
+ count: namespace.number_of_documents ?? 0,
218
+ }
219
+ deleteModalVisible.value = true
220
+ }
221
+
222
+ // The nested document route renders inside this page, so deleting what it points at leaves it mounted on a
223
+ // dead URL. Leave for the nearest surviving ancestor first: navigating deactivates the child's queries, so the
224
+ // delete's invalidation only marks them stale instead of refetching a resource the teardown job is purging.
225
+ // Both cases land on the database list — there is no /service/knowledge/[db] route.
226
+ const isViewingPendingDeletion = (pending: PendingDeletion) => {
227
+ if (route.params.db !== pending.database) return false
228
+ return pending.type === 'database' || route.params.namespace === pending.namespace
229
+ }
230
+
231
+ const handleConfirmDelete = async () => {
232
+ const pending = pendingDeletion.value
233
+ if (!pending) return
234
+
235
+ try {
236
+ if (isViewingPendingDeletion(pending)) {
237
+ await router.push(tenantPath('/service/knowledge'))
238
+ }
239
+
240
+ if (pending.type === 'database') {
241
+ await deleteDatabase({ tenantId: tenantId.value!, database: pending.database })
242
+ }
243
+ else {
244
+ await deleteNamespace({ tenantId: tenantId.value!, database: pending.database, namespace: pending.namespace })
245
+ }
246
+ toast.add({
247
+ severity: 'success',
248
+ summary: t('knowledge.delete.scheduled.summary'),
249
+ detail: t('knowledge.delete.scheduled.detail'),
250
+ life: 4000,
251
+ })
252
+ deleteModalVisible.value = false
253
+ pendingDeletion.value = null
254
+ }
255
+ catch {
256
+ toast.add({
257
+ severity: 'error',
258
+ summary: t('knowledge.delete.error.summary'),
259
+ detail: t('knowledge.delete.error.detail'),
260
+ life: 4000,
261
+ })
262
+ }
263
+ }
126
264
  </script>
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <AuthLoginPanel>
3
+ <template #heading>
4
+ <template v-if="provider">
5
+ {{ t('auth.login.welcomeProvider', { provider: provider.display_name }) }}
6
+ </template>
7
+ </template>
8
+ <template #message>
9
+ <template v-if="provider">
10
+ {{ t('auth.login.pleaseLoginWith', { provider: provider.display_name }) }}
11
+ </template>
12
+ </template>
13
+
14
+ <Button
15
+ v-if="provider"
16
+ :label="t('auth.login.loginWith', { provider: provider.display_name })"
17
+ :icon="`pi ${provider.icon}`"
18
+ icon-pos="right"
19
+ class="!bg-white !text-black"
20
+ @click="login(provider.alias)"
21
+ />
22
+ <ProgressSpinner
23
+ v-else
24
+ class="!h-8 !w-8"
25
+ />
26
+ </AuthLoginPanel>
27
+ </template>
28
+
29
+ <script setup lang="ts">
30
+ definePageMeta({
31
+ layout: 'anonymous',
32
+ })
33
+
34
+ const { t, locale } = useI18n()
35
+ const route = useRoute()
36
+ const { login } = useAuth()
37
+ const { authProviders, isLoading } = useAuthProviders()
38
+
39
+ /**
40
+ * The empty alias belongs to the synthetic "Keycloak" entry, which has no
41
+ * kc_idp_hint and must therefore not be addressable through a tenant link.
42
+ */
43
+ const requestedAlias = computed(() => String(route.params.idp ?? ''))
44
+ const provider = computed(() =>
45
+ requestedAlias.value
46
+ ? authProviders.value?.find(candidate => candidate.alias === requestedAlias.value)
47
+ : undefined,
48
+ )
49
+
50
+ // Only decide once the query settled — an unknown, disabled or hidden alias
51
+ // (and a failed provider request) falls back to the all-providers page.
52
+ watch([isLoading, provider], ([providersLoading, matchedProvider]) => {
53
+ if (!providersLoading && !matchedProvider) {
54
+ navigateTo(`/${locale.value}/auth/login`, { replace: true })
55
+ }
56
+ }, { immediate: true })
57
+ </script>
@@ -0,0 +1,46 @@
1
+ <template>
2
+ <AuthLoginPanel>
3
+ <template #heading>
4
+ {{ t('auth.login.welcome', { companyName }) }}
5
+ </template>
6
+ <template #message>
7
+ {{ t('auth.login.pleaseLogin') }}
8
+ </template>
9
+
10
+ <ProgressSpinner
11
+ v-if="isLoading"
12
+ class="!h-8 !w-8"
13
+ />
14
+ <template v-else>
15
+ <Button
16
+ v-for="idp in authProviders ?? []"
17
+ :key="idp.alias"
18
+ :label="t('auth.login.loginWith', { provider: idp.display_name })"
19
+ :icon="`pi ${idp.icon}`"
20
+ icon-pos="right"
21
+ class="!bg-white !text-black"
22
+ @click="login(idp.alias || undefined)"
23
+ />
24
+ <Button
25
+ v-if="(authProviders?.length ?? 0) === 0"
26
+ :label="t('auth.login.title')"
27
+ icon="pi pi-sign-in"
28
+ icon-pos="right"
29
+ class="!bg-white !text-black"
30
+ @click="login()"
31
+ />
32
+ </template>
33
+ </AuthLoginPanel>
34
+ </template>
35
+
36
+ <script setup lang="ts">
37
+ definePageMeta({
38
+ layout: 'anonymous',
39
+ })
40
+
41
+ const { t } = useI18n()
42
+ const { login } = useAuth()
43
+ const { authProviders, isLoading } = useAuthProviders()
44
+
45
+ const companyName = 'bbv Software Services AG'
46
+ </script>
@@ -50,6 +50,11 @@ export default defineNuxtPlugin(() => {
50
50
  assign(group('sysadmin'), 'url', injected.SYSADMIN_URL)
51
51
  assign(group('mainApp'), 'url', injected.MAIN_APP_URL)
52
52
  if (injected.API_BASE_URL) publicConfig.apiBaseUrl = injected.API_BASE_URL
53
+ // The version actually deployed. A release is promoted by retagging the exact
54
+ // `-rc.N` build that was tested, so whatever was baked into the bundle at build
55
+ // time keeps the candidate's name — the deployment is the only party that knows
56
+ // the tag it pulled. Falls back to the baked value when nothing is injected.
57
+ if (injected.APP_VERSION) publicConfig.appVersion = injected.APP_VERSION
53
58
 
54
59
  isConfigLoaded.value = true
55
60
  })
@@ -12,7 +12,7 @@ export default defineNuxtPlugin(async ({ $i18n, $router }) => {
12
12
  client_id: config.public.oidc.clientId,
13
13
  redirect_uri: `${globalThis.location.origin}/${$i18n.locale.value}/auth/callback`,
14
14
  silent_redirect_uri: `${globalThis.location.origin}/${$i18n.locale.value}/auth/renew`,
15
- post_logout_redirect_uri: globalThis.location.origin,
15
+ post_logout_redirect_uri: `${globalThis.location.origin}/${$i18n.locale.value}/auth/login`,
16
16
  response_type: 'code',
17
17
  scope: 'openid profile email',
18
18
  filterProtocolClaims: true,
@@ -197,9 +197,7 @@ export const createClient = (config: Config = {}): Client => {
197
197
  method,
198
198
  onRequest: undefined,
199
199
  serializedBody: getValidRequestBody(opts) as
200
- | BodyInit
201
- | null
202
- | undefined,
200
+ BodyInit | null | undefined,
203
201
  signal: unwrapRefs(opts.signal) as AbortSignal,
204
202
  url,
205
203
  });
@@ -33,14 +33,12 @@ export type QuerySerializer = (
33
33
 
34
34
  type WithRefs<TData> = {
35
35
  [K in keyof TData]: NonNullable<TData[K]> extends object
36
- ?
37
- | WithRefs<NonNullable<TData[K]>>
38
- | Ref<NonNullable<TData[K]>>
39
- | Extract<TData[K], null>
40
- :
41
- | NonNullable<TData[K]>
42
- | Ref<NonNullable<TData[K]>>
43
- | Extract<TData[K], null>;
36
+ ? | WithRefs<NonNullable<TData[K]>>
37
+ | Ref<NonNullable<TData[K]>>
38
+ | Extract<TData[K], null>
39
+ : | NonNullable<TData[K]>
40
+ | Ref<NonNullable<TData[K]>>
41
+ | Extract<TData[K], null>;
44
42
  };
45
43
 
46
44
  // copied from Nuxt
@@ -206,8 +204,4 @@ type FetchOptions<TData> = Omit<
206
204
  >;
207
205
 
208
206
  export type Composable =
209
- | "$fetch"
210
- | "useAsyncData"
211
- | "useFetch"
212
- | "useLazyAsyncData"
213
- | "useLazyFetch";
207
+ "$fetch" | "useAsyncData" | "useFetch" | "useLazyAsyncData" | "useLazyFetch";
@@ -195,8 +195,7 @@ export const setAuthParams = async ({
195
195
  options.query = {};
196
196
  }
197
197
  const queryValue = toValue(options.query) as
198
- | Record<string, unknown>
199
- | undefined;
198
+ Record<string, unknown> | undefined;
200
199
  if (queryValue) {
201
200
  queryValue[name] = token;
202
201
  }
@@ -4,12 +4,7 @@
4
4
  * JSON-friendly union that mirrors what Pinia Colada can hash.
5
5
  */
6
6
  export type JsonValue =
7
- | null
8
- | string
9
- | number
10
- | boolean
11
- | JsonValue[]
12
- | { [key: string]: JsonValue };
7
+ null | string | number | boolean | JsonValue[] | { [key: string]: JsonValue };
13
8
 
14
9
  /**
15
10
  * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
@@ -112,7 +112,9 @@ type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
112
112
  : false;
113
113
 
114
114
  export type OmitNever<T extends Record<string, unknown>> = {
115
- [K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true
116
- ? never
117
- : K]: T[K];
115
+ [
116
+ K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true
117
+ ? never
118
+ : K
119
+ ]: T[K];
118
120
  };