From 7a3932f96921137fe9bc812c50daede3aa7e0f04 Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Fri, 27 Feb 2026 23:13:54 +0100 Subject: [PATCH] feat: OIDC (#564) * feat: oidc feat: organization switcher refactor: org context feat: invitations GLM * feat: link current account * refactor: own page for sso registration * feat: per-user account management * refactor: code style * refactor: user existing check * refactor: restrict provider configuration to super admins only * refactor: cleanup / pr review * chore: fix lint issues * chore: pr feedbacks * test(e2e): automated tests for OIDC * fix: check url first for sso provider identification * fix: prevent oidc provider to be named "credential" --- .github/workflows/e2e.yml | 10 +- .gitignore | 2 + .oxfmtrc.json | 3 +- AGENTS.md | 7 +- .../api-client/@tanstack/react-query.gen.ts | 2329 ++--- app/client/api-client/client/client.gen.ts | 483 +- app/client/api-client/client/types.gen.ts | 303 +- app/client/api-client/client/utils.gen.ts | 463 +- app/client/api-client/core/auth.gen.ts | 54 +- .../api-client/core/bodySerializer.gen.ts | 104 +- app/client/api-client/core/params.gen.ts | 260 +- .../api-client/core/pathSerializer.gen.ts | 254 +- .../api-client/core/queryKeySerializer.gen.ts | 136 +- .../api-client/core/serverSentEvents.gen.ts | 400 +- app/client/api-client/core/types.gen.ts | 162 +- app/client/api-client/core/utils.gen.ts | 211 +- app/client/api-client/index.ts | 28 + app/client/api-client/sdk.gen.ts | 863 +- app/client/api-client/types.gen.ts | 8380 ++++++++--------- app/client/components/app-sidebar.tsx | 4 +- app/client/components/grid-background.tsx | 2 +- app/client/components/layout.tsx | 2 +- .../components/organization-switcher.tsx | 110 + app/client/components/ui/card.tsx | 2 +- app/client/components/ui/dropdown-menu.tsx | 217 + app/client/functions/get-origin.ts | 6 + app/client/hooks/use-org-context.ts | 13 + app/client/lib/auth-client.ts | 2 + app/client/lib/auth-errors.ts | 62 + .../auth/routes/__tests__/login.test.tsx | 57 + app/client/modules/auth/routes/login.tsx | 61 +- .../components/sso/sso-settings-section.tsx | 366 + .../settings/components/user-management.tsx | 199 +- .../settings/routes/create-sso-provider.tsx | 258 + .../modules/settings/routes/settings.tsx | 20 +- app/context.ts | 1 + .../migration.sql | 15 + .../snapshot.json | 2185 +++++ app/middleware/auth.ts | 21 +- app/routeTree.gen.ts | 114 +- app/routes/(auth)/login.error.tsx | 23 + app/routes/(auth)/login.tsx | 17 +- app/routes/(auth)/route.tsx | 9 + app/routes/(dashboard)/settings/index.tsx | 11 +- app/routes/(dashboard)/settings/sso/new.tsx | 32 + app/routes/api.$.ts | 6 +- app/server/db/relations.ts | 14 + app/server/db/schema.ts | 21 + app/server/lib/auth.ts | 104 +- .../__tests__/create-default-org.test.ts | 161 + .../lib/auth/helpers/create-default-org.ts | 172 + .../__tests__/convert-legacy-user.test.ts | 8 +- .../__tests__/require-sso-invitation.test.ts | 142 + .../trust-sso-provider-for-linking.test.ts | 215 + .../validate-sso-callback-urls.test.ts | 74 + .../validate-sso-provider-id.test.ts | 42 + .../middlewares}/convert-legacy-user.ts | 2 +- .../middlewares}/only-one-user.ts | 5 +- .../middlewares/require-sso-invitation.ts | 52 + .../trust-sso-provider-for-linking.ts | 43 + .../middlewares/validate-sso-callback-urls.ts | 36 + .../middlewares/validate-sso-provider-id.ts | 25 + .../plugins/sso-trusted-provider-linking.ts | 21 + app/server/lib/auth/utils/sso-context.ts | 31 + app/server/lib/auth/utils/sso-provider-id.ts | 9 + .../lib/functions/organization-context.ts | 31 + .../auth.service.sso-provider.test.ts | 177 + .../auth/__tests__/auth.sso-security.test.ts | 94 + app/server/modules/auth/auth.controller.ts | 141 +- app/server/modules/auth/auth.dto.ts | 165 + app/server/modules/auth/auth.middleware.ts | 1 + app/server/modules/auth/auth.service.ts | 156 +- bun.lock | 234 +- docker-compose.yml | 12 + e2e/0002-backup-restore.spec.ts | 108 +- e2e/0003-oidc.spec.ts | 167 + package.json | 4 +- playwright.config.ts | 6 +- playwright/dex-config.yaml | 50 + 79 files changed, 12915 insertions(+), 7875 deletions(-) create mode 100644 app/client/components/organization-switcher.tsx create mode 100644 app/client/components/ui/dropdown-menu.tsx create mode 100644 app/client/functions/get-origin.ts create mode 100644 app/client/hooks/use-org-context.ts create mode 100644 app/client/lib/auth-errors.ts create mode 100644 app/client/modules/auth/routes/__tests__/login.test.tsx create mode 100644 app/client/modules/settings/components/sso/sso-settings-section.tsx create mode 100644 app/client/modules/settings/routes/create-sso-provider.tsx create mode 100644 app/drizzle/20260227200731_sad_luke_cage/migration.sql create mode 100644 app/drizzle/20260227200731_sad_luke_cage/snapshot.json create mode 100644 app/routes/(auth)/login.error.tsx create mode 100644 app/routes/(auth)/route.tsx create mode 100644 app/routes/(dashboard)/settings/sso/new.tsx create mode 100644 app/server/lib/auth/helpers/__tests__/create-default-org.test.ts create mode 100644 app/server/lib/auth/helpers/create-default-org.ts rename app/server/lib/{auth-middlewares => auth/middlewares}/__tests__/convert-legacy-user.test.ts (97%) create mode 100644 app/server/lib/auth/middlewares/__tests__/require-sso-invitation.test.ts create mode 100644 app/server/lib/auth/middlewares/__tests__/trust-sso-provider-for-linking.test.ts create mode 100644 app/server/lib/auth/middlewares/__tests__/validate-sso-callback-urls.test.ts create mode 100644 app/server/lib/auth/middlewares/__tests__/validate-sso-provider-id.test.ts rename app/server/lib/{auth-middlewares => auth/middlewares}/convert-legacy-user.ts (98%) rename app/server/lib/{auth-middlewares => auth/middlewares}/only-one-user.ts (92%) create mode 100644 app/server/lib/auth/middlewares/require-sso-invitation.ts create mode 100644 app/server/lib/auth/middlewares/trust-sso-provider-for-linking.ts create mode 100644 app/server/lib/auth/middlewares/validate-sso-callback-urls.ts create mode 100644 app/server/lib/auth/middlewares/validate-sso-provider-id.ts create mode 100644 app/server/lib/auth/plugins/sso-trusted-provider-linking.ts create mode 100644 app/server/lib/auth/utils/sso-context.ts create mode 100644 app/server/lib/auth/utils/sso-provider-id.ts create mode 100644 app/server/lib/functions/organization-context.ts create mode 100644 app/server/modules/auth/__tests__/auth.service.sso-provider.test.ts create mode 100644 app/server/modules/auth/__tests__/auth.sso-security.test.ts create mode 100644 e2e/0003-oidc.spec.ts create mode 100644 playwright/dex-config.yaml diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index f073918d..a8b203f4 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -51,6 +51,7 @@ jobs: echo "SERVER_IP=localhost" >> .env.local echo "ZEROBYTE_DATABASE_URL=./data/zerobyte.db" >> .env.local echo "BASE_URL=http://localhost:4096" >> .env.local + echo "E2E_DEX_ORIGIN=http://dex:5557" >> .env.local - name: Start zerobyte-e2e service run: bun run start:e2e -- -d @@ -59,9 +60,13 @@ jobs: run: | timeout 30s bash -c 'until curl -f http://localhost:4096/api/healthcheck; do echo "Waiting for server..." && sleep 2; done' + - name: Wait for Dex to be ready + run: | + timeout 30s bash -c 'until curl -sf http://localhost:5557/dex/.well-known/openid-configuration; do echo "Waiting for Dex..." && sleep 2; done' + - name: Print docker logs if failed to start if: failure() - run: docker logs zerobyte || true + run: docker compose logs zerobyte-e2e dex || true - name: Make playwright directory writable run: sudo chmod 777 playwright/data/zerobyte.db @@ -73,7 +78,8 @@ jobs: if: always() run: | tree playwright - docker logs zerobyte > playwright-report/container-logs.txt || true + docker compose logs zerobyte-e2e > playwright-report/container-logs.txt || true + docker compose logs dex > playwright-report/dex-logs.txt || true - name: Debug - print content of /test-data in container if: failure() diff --git a/.gitignore b/.gitignore index 1e5f011a..8122a126 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ node_modules/ /blob-report/ /playwright/.cache/ /playwright/.auth/ +/playwright/restic.pass playwright/.auth playwright/temp @@ -39,3 +40,4 @@ openapi-ts-error-*.log .output tmp/ qa-output + diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 988352d8..17174df4 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -2,5 +2,6 @@ "$schema": "./node_modules/oxfmt/configuration_schema.json", "printWidth": 120, "useTabs": true, - "endOfLine": "lf" + "endOfLine": "lf", + "ignorePatterns": ["*.gen.ts"] } diff --git a/AGENTS.md b/AGENTS.md index c1891294..2d1f8237 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,3 @@ -# AGENTS.md - ## Important instructions - Never create migration files manually. Always use the provided command to generate migrations @@ -62,3 +60,8 @@ bunx oxfmt format --write # Lint bun run lint ``` + +### Invalidation + +The frontend has an automatic invalidation setup which runs after every mutation. +Do not implement any invalidation logic in the frontend. diff --git a/app/client/api-client/@tanstack/react-query.gen.ts b/app/client/api-client/@tanstack/react-query.gen.ts index 919f4fa3..b5d7343b 100644 --- a/app/client/api-client/@tanstack/react-query.gen.ts +++ b/app/client/api-client/@tanstack/react-query.gen.ts @@ -1,1671 +1,1310 @@ // @ts-nocheck // This file is auto-generated by @hey-api/openapi-ts -import { - type DefaultError, - type InfiniteData, - infiniteQueryOptions, - queryOptions, - type UseMutationOptions, -} from "@tanstack/react-query"; +import { type DefaultError, type InfiniteData, infiniteQueryOptions, queryOptions, type UseMutationOptions } from '@tanstack/react-query'; -import { client } from "../client.gen"; -import { - browseFilesystem, - cancelDoctor, - createBackupSchedule, - createNotificationDestination, - createRepository, - createVolume, - deleteBackupSchedule, - deleteNotificationDestination, - deleteRepository, - deleteSnapshot, - deleteSnapshots, - deleteVolume, - devPanelExec, - downloadResticPassword, - dumpSnapshot, - getBackupProgress, - getBackupSchedule, - getBackupScheduleForVolume, - getDevPanel, - getMirrorCompatibility, - getNotificationDestination, - getRegistrationStatus, - getRepository, - getRepositoryStats, - getScheduleMirrors, - getScheduleNotifications, - getSnapshotDetails, - getStatus, - getSystemInfo, - getUpdates, - getUserDeletionImpact, - getVolume, - healthCheckVolume, - listBackupSchedules, - listFiles, - listNotificationDestinations, - listRcloneRemotes, - listRepositories, - listSnapshotFiles, - listSnapshots, - listVolumes, - mountVolume, - type Options, - refreshSnapshots, - reorderBackupSchedules, - restoreSnapshot, - runBackupNow, - runForget, - setRegistrationStatus, - startDoctor, - stopBackup, - tagSnapshots, - testConnection, - testNotificationDestination, - unlockRepository, - unmountVolume, - updateBackupSchedule, - updateNotificationDestination, - updateRepository, - updateScheduleMirrors, - updateScheduleNotifications, - updateVolume, -} from "../sdk.gen"; -import type { - BrowseFilesystemData, - BrowseFilesystemResponse, - CancelDoctorData, - CancelDoctorResponse, - CreateBackupScheduleData, - CreateBackupScheduleResponse, - CreateNotificationDestinationData, - CreateNotificationDestinationResponse, - CreateRepositoryData, - CreateRepositoryResponse, - CreateVolumeData, - CreateVolumeResponse, - DeleteBackupScheduleData, - DeleteBackupScheduleResponse, - DeleteNotificationDestinationData, - DeleteNotificationDestinationResponse, - DeleteRepositoryData, - DeleteRepositoryResponse, - DeleteSnapshotData, - DeleteSnapshotResponse, - DeleteSnapshotsData, - DeleteSnapshotsResponse, - DeleteVolumeData, - DeleteVolumeResponse, - DevPanelExecData, - DevPanelExecResponse, - DownloadResticPasswordData, - DownloadResticPasswordResponse, - DumpSnapshotData, - DumpSnapshotResponse, - GetBackupProgressData, - GetBackupProgressResponse, - GetBackupScheduleData, - GetBackupScheduleForVolumeData, - GetBackupScheduleForVolumeResponse, - GetBackupScheduleResponse, - GetDevPanelData, - GetDevPanelResponse, - GetMirrorCompatibilityData, - GetMirrorCompatibilityResponse, - GetNotificationDestinationData, - GetNotificationDestinationResponse, - GetRegistrationStatusData, - GetRegistrationStatusResponse, - GetRepositoryData, - GetRepositoryResponse, - GetRepositoryStatsData, - GetRepositoryStatsResponse, - GetScheduleMirrorsData, - GetScheduleMirrorsResponse, - GetScheduleNotificationsData, - GetScheduleNotificationsResponse, - GetSnapshotDetailsData, - GetSnapshotDetailsResponse, - GetStatusData, - GetStatusResponse, - GetSystemInfoData, - GetSystemInfoResponse, - GetUpdatesData, - GetUpdatesResponse, - GetUserDeletionImpactData, - GetUserDeletionImpactResponse, - GetVolumeData, - GetVolumeResponse, - HealthCheckVolumeData, - HealthCheckVolumeResponse, - ListBackupSchedulesData, - ListBackupSchedulesResponse, - ListFilesData, - ListFilesResponse, - ListNotificationDestinationsData, - ListNotificationDestinationsResponse, - ListRcloneRemotesData, - ListRcloneRemotesResponse, - ListRepositoriesData, - ListRepositoriesResponse, - ListSnapshotFilesData, - ListSnapshotFilesResponse, - ListSnapshotsData, - ListSnapshotsResponse, - ListVolumesData, - ListVolumesResponse, - MountVolumeData, - MountVolumeResponse, - RefreshSnapshotsData, - RefreshSnapshotsResponse, - ReorderBackupSchedulesData, - ReorderBackupSchedulesResponse, - RestoreSnapshotData, - RestoreSnapshotResponse, - RunBackupNowData, - RunBackupNowResponse, - RunForgetData, - RunForgetResponse, - SetRegistrationStatusData, - SetRegistrationStatusResponse, - StartDoctorData, - StartDoctorResponse, - StopBackupData, - StopBackupResponse, - TagSnapshotsData, - TagSnapshotsResponse, - TestConnectionData, - TestConnectionResponse, - TestNotificationDestinationData, - TestNotificationDestinationResponse, - UnlockRepositoryData, - UnlockRepositoryResponse, - UnmountVolumeData, - UnmountVolumeResponse, - UpdateBackupScheduleData, - UpdateBackupScheduleResponse, - UpdateNotificationDestinationData, - UpdateNotificationDestinationResponse, - UpdateRepositoryData, - UpdateRepositoryResponse, - UpdateScheduleMirrorsData, - UpdateScheduleMirrorsResponse, - UpdateScheduleNotificationsData, - UpdateScheduleNotificationsResponse, - UpdateVolumeData, - UpdateVolumeResponse, -} from "../types.gen"; +import { client } from '../client.gen'; +import { browseFilesystem, cancelDoctor, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteSnapshots, deleteSsoInvitation, deleteSsoProvider, deleteUserAccount, deleteVolume, devPanelExec, downloadResticPassword, dumpSnapshot, getAdminUsers, getBackupProgress, getBackupSchedule, getBackupScheduleForVolume, getDevPanel, getMirrorCompatibility, getNotificationDestination, getPublicSsoProviders, getRegistrationStatus, getRepository, getRepositoryStats, getScheduleMirrors, getScheduleNotifications, getSnapshotDetails, getSsoSettings, getStatus, getSystemInfo, getUpdates, getUserDeletionImpact, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, mountVolume, type Options, refreshSnapshots, reorderBackupSchedules, restoreSnapshot, runBackupNow, runForget, setRegistrationStatus, startDoctor, stopBackup, tagSnapshots, testConnection, testNotificationDestination, unlockRepository, unmountVolume, updateBackupSchedule, updateNotificationDestination, updateRepository, updateScheduleMirrors, updateScheduleNotifications, updateSsoProviderAutoLinking, updateVolume } from '../sdk.gen'; +import type { BrowseFilesystemData, BrowseFilesystemResponse, CancelDoctorData, CancelDoctorResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteSnapshotsData, DeleteSnapshotsResponse, DeleteSsoInvitationData, DeleteSsoProviderData, DeleteUserAccountData, DeleteVolumeData, DeleteVolumeResponse, DevPanelExecData, DevPanelExecResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, DumpSnapshotData, DumpSnapshotResponse, GetAdminUsersData, GetAdminUsersResponse, GetBackupProgressData, GetBackupProgressResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetDevPanelData, GetDevPanelResponse, GetMirrorCompatibilityData, GetMirrorCompatibilityResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetPublicSsoProvidersData, GetPublicSsoProvidersResponse, GetRegistrationStatusData, GetRegistrationStatusResponse, GetRepositoryData, GetRepositoryResponse, GetRepositoryStatsData, GetRepositoryStatsResponse, GetScheduleMirrorsData, GetScheduleMirrorsResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetSsoSettingsData, GetSsoSettingsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetUpdatesData, GetUpdatesResponse, GetUserDeletionImpactData, GetUserDeletionImpactResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, ListNotificationDestinationsData, ListNotificationDestinationsResponse, ListRcloneRemotesData, ListRcloneRemotesResponse, ListRepositoriesData, ListRepositoriesResponse, ListSnapshotFilesData, ListSnapshotFilesResponse, ListSnapshotsData, ListSnapshotsResponse, ListVolumesData, ListVolumesResponse, MountVolumeData, MountVolumeResponse, RefreshSnapshotsData, RefreshSnapshotsResponse, ReorderBackupSchedulesData, ReorderBackupSchedulesResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, SetRegistrationStatusData, SetRegistrationStatusResponse, StartDoctorData, StartDoctorResponse, StopBackupData, StopBackupResponse, TagSnapshotsData, TagSnapshotsResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnlockRepositoryData, UnlockRepositoryResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponse, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateSsoProviderAutoLinkingData, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen'; export type QueryKey = [ - Pick & { - _id: string; - _infinite?: boolean; - tags?: ReadonlyArray; - }, + Pick & { + _id: string; + _infinite?: boolean; + tags?: ReadonlyArray; + } ]; -const createQueryKey = ( - id: string, - options?: TOptions, - infinite?: boolean, - tags?: ReadonlyArray, -): [QueryKey[0]] => { - const params: QueryKey[0] = { - _id: id, - baseUrl: options?.baseUrl || (options?.client ?? client).getConfig().baseUrl, - } as QueryKey[0]; - if (infinite) { - params._infinite = infinite; - } - if (tags) { - params.tags = tags; - } - if (options?.body) { - params.body = options.body; - } - if (options?.headers) { - params.headers = options.headers; - } - if (options?.path) { - params.path = options.path; - } - if (options?.query) { - params.query = options.query; - } - return [params]; +const createQueryKey = (id: string, options?: TOptions, infinite?: boolean, tags?: ReadonlyArray): [ + QueryKey[0] +] => { + const params: QueryKey[0] = { _id: id, baseUrl: options?.baseUrl || (options?.client ?? client).getConfig().baseUrl } as QueryKey[0]; + if (infinite) { + params._infinite = infinite; + } + if (tags) { + params.tags = tags; + } + if (options?.body) { + params.body = options.body; + } + if (options?.headers) { + params.headers = options.headers; + } + if (options?.path) { + params.path = options.path; + } + if (options?.query) { + params.query = options.query; + } + return [params]; }; -export const getStatusQueryKey = (options?: Options) => createQueryKey("getStatus", options); +export const getStatusQueryKey = (options?: Options) => createQueryKey('getStatus', options); /** * Get authentication system status */ -export const getStatusOptions = (options?: Options) => - queryOptions>({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getStatus({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: getStatusQueryKey(options), - }); +export const getStatusOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getStatus({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getStatusQueryKey(options) +}); -export const getUserDeletionImpactQueryKey = (options: Options) => - createQueryKey("getUserDeletionImpact", options); +export const getPublicSsoProvidersQueryKey = (options?: Options) => createQueryKey('getPublicSsoProviders', options); + +/** + * Get public SSO providers for the instance + */ +export const getPublicSsoProvidersOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getPublicSsoProviders({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getPublicSsoProvidersQueryKey(options) +}); + +export const getSsoSettingsQueryKey = (options?: Options) => createQueryKey('getSsoSettings', options); + +/** + * Get SSO providers and invitations for the active organization + */ +export const getSsoSettingsOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getSsoSettings({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getSsoSettingsQueryKey(options) +}); + +/** + * Delete an SSO provider + */ +export const deleteSsoProviderMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await deleteSsoProvider({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Update whether SSO sign-in can auto-link existing accounts by email + */ +export const updateSsoProviderAutoLinkingMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await updateSsoProviderAutoLinking({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Delete an SSO invitation + */ +export const deleteSsoInvitationMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await deleteSsoInvitation({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getAdminUsersQueryKey = (options?: Options) => createQueryKey('getAdminUsers', options); + +/** + * List admin users for settings management + */ +export const getAdminUsersOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getAdminUsers({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getAdminUsersQueryKey(options) +}); + +/** + * Delete an account linked to a user + */ +export const deleteUserAccountMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await deleteUserAccount({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getUserDeletionImpactQueryKey = (options: Options) => createQueryKey('getUserDeletionImpact', options); /** * Get impact of deleting a user */ -export const getUserDeletionImpactOptions = (options: Options) => - queryOptions< - GetUserDeletionImpactResponse, - DefaultError, - GetUserDeletionImpactResponse, - ReturnType - >({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getUserDeletionImpact({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: getUserDeletionImpactQueryKey(options), - }); +export const getUserDeletionImpactOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getUserDeletionImpact({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getUserDeletionImpactQueryKey(options) +}); -export const listVolumesQueryKey = (options?: Options) => createQueryKey("listVolumes", options); +export const listVolumesQueryKey = (options?: Options) => createQueryKey('listVolumes', options); /** * List all volumes */ -export const listVolumesOptions = (options?: Options) => - queryOptions>({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await listVolumes({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: listVolumesQueryKey(options), - }); +export const listVolumesOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await listVolumes({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: listVolumesQueryKey(options) +}); /** * Create a new volume */ -export const createVolumeMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await createVolume({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const createVolumeMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await createVolume({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; /** * Test connection to backend */ -export const testConnectionMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await testConnection({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const testConnectionMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await testConnection({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; /** * Delete a volume */ -export const deleteVolumeMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await deleteVolume({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const deleteVolumeMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await deleteVolume({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; -export const getVolumeQueryKey = (options: Options) => createQueryKey("getVolume", options); +export const getVolumeQueryKey = (options: Options) => createQueryKey('getVolume', options); /** * Get a volume by name */ -export const getVolumeOptions = (options: Options) => - queryOptions>({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getVolume({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: getVolumeQueryKey(options), - }); +export const getVolumeOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getVolume({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getVolumeQueryKey(options) +}); /** * Update a volume's configuration */ -export const updateVolumeMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await updateVolume({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const updateVolumeMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await updateVolume({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; /** * Mount a volume */ -export const mountVolumeMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await mountVolume({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const mountVolumeMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await mountVolume({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; /** * Unmount a volume */ -export const unmountVolumeMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await unmountVolume({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const unmountVolumeMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await unmountVolume({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; /** * Perform a health check on a volume */ -export const healthCheckVolumeMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await healthCheckVolume({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const healthCheckVolumeMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await healthCheckVolume({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; -export const listFilesQueryKey = (options: Options) => createQueryKey("listFiles", options); +export const listFilesQueryKey = (options: Options) => createQueryKey('listFiles', options); /** * List files in a volume directory */ -export const listFilesOptions = (options: Options) => - queryOptions>({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await listFiles({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: listFilesQueryKey(options), - }); +export const listFilesOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await listFiles({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: listFilesQueryKey(options) +}); -const createInfiniteParams = [0], "body" | "headers" | "path" | "query">>( - queryKey: QueryKey, - page: K, -) => { - const params = { ...queryKey[0] }; - if (page.body) { - params.body = { - ...(queryKey[0].body as any), - ...(page.body as any), - }; - } - if (page.headers) { - params.headers = { - ...queryKey[0].headers, - ...page.headers, - }; - } - if (page.path) { - params.path = { - ...(queryKey[0].path as any), - ...(page.path as any), - }; - } - if (page.query) { - params.query = { - ...(queryKey[0].query as any), - ...(page.query as any), - }; - } - return params as unknown as typeof page; +const createInfiniteParams = [0], 'body' | 'headers' | 'path' | 'query'>>(queryKey: QueryKey, page: K) => { + const params = { ...queryKey[0] }; + if (page.body) { + params.body = { + ...queryKey[0].body as any, + ...page.body as any + }; + } + if (page.headers) { + params.headers = { + ...queryKey[0].headers, + ...page.headers + }; + } + if (page.path) { + params.path = { + ...queryKey[0].path as any, + ...page.path as any + }; + } + if (page.query) { + params.query = { + ...queryKey[0].query as any, + ...page.query as any + }; + } + return params as unknown as typeof page; }; -export const listFilesInfiniteQueryKey = (options: Options): QueryKey> => - createQueryKey("listFiles", options, true); +export const listFilesInfiniteQueryKey = (options: Options): QueryKey> => createQueryKey('listFiles', options, true); /** * List files in a volume directory */ -export const listFilesInfiniteOptions = (options: Options) => - infiniteQueryOptions< - ListFilesResponse, - DefaultError, - InfiniteData, - QueryKey>, - string | Pick>[0], "body" | "headers" | "path" | "query"> - >( - // @ts-ignore - { - queryFn: async ({ pageParam, queryKey, signal }) => { - // @ts-ignore - const page: Pick>[0], "body" | "headers" | "path" | "query"> = - typeof pageParam === "object" - ? pageParam - : { - query: { - offset: pageParam, - }, - }; - const params = createInfiniteParams(queryKey, page); - const { data } = await listFiles({ - ...options, - ...params, - signal, - throwOnError: true, - }); - return data; - }, - queryKey: listFilesInfiniteQueryKey(options), - }, - ); +export const listFilesInfiniteOptions = (options: Options) => infiniteQueryOptions, QueryKey>, string | Pick>[0], 'body' | 'headers' | 'path' | 'query'>>( +// @ts-ignore +{ + queryFn: async ({ pageParam, queryKey, signal }) => { + // @ts-ignore + const page: Pick>[0], 'body' | 'headers' | 'path' | 'query'> = typeof pageParam === 'object' ? pageParam : { + query: { + offset: pageParam + } + }; + const params = createInfiniteParams(queryKey, page); + const { data } = await listFiles({ + ...options, + ...params, + signal, + throwOnError: true + }); + return data; + }, + queryKey: listFilesInfiniteQueryKey(options) +}); -export const browseFilesystemQueryKey = (options?: Options) => - createQueryKey("browseFilesystem", options); +export const browseFilesystemQueryKey = (options?: Options) => createQueryKey('browseFilesystem', options); /** * Browse directories on the host filesystem */ -export const browseFilesystemOptions = (options?: Options) => - queryOptions< - BrowseFilesystemResponse, - DefaultError, - BrowseFilesystemResponse, - ReturnType - >({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await browseFilesystem({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: browseFilesystemQueryKey(options), - }); +export const browseFilesystemOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await browseFilesystem({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: browseFilesystemQueryKey(options) +}); -export const listRepositoriesQueryKey = (options?: Options) => - createQueryKey("listRepositories", options); +export const listRepositoriesQueryKey = (options?: Options) => createQueryKey('listRepositories', options); /** * List all repositories */ -export const listRepositoriesOptions = (options?: Options) => - queryOptions< - ListRepositoriesResponse, - DefaultError, - ListRepositoriesResponse, - ReturnType - >({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await listRepositories({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: listRepositoriesQueryKey(options), - }); +export const listRepositoriesOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await listRepositories({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: listRepositoriesQueryKey(options) +}); /** * Create a new restic repository */ -export const createRepositoryMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await createRepository({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const createRepositoryMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await createRepository({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; -export const listRcloneRemotesQueryKey = (options?: Options) => - createQueryKey("listRcloneRemotes", options); +export const listRcloneRemotesQueryKey = (options?: Options) => createQueryKey('listRcloneRemotes', options); /** * List all configured rclone remotes on the host system */ -export const listRcloneRemotesOptions = (options?: Options) => - queryOptions< - ListRcloneRemotesResponse, - DefaultError, - ListRcloneRemotesResponse, - ReturnType - >({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await listRcloneRemotes({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: listRcloneRemotesQueryKey(options), - }); +export const listRcloneRemotesOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await listRcloneRemotes({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: listRcloneRemotesQueryKey(options) +}); /** * Delete a repository */ -export const deleteRepositoryMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await deleteRepository({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const deleteRepositoryMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await deleteRepository({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; -export const getRepositoryQueryKey = (options: Options) => createQueryKey("getRepository", options); +export const getRepositoryQueryKey = (options: Options) => createQueryKey('getRepository', options); /** * Get a single repository by ID */ -export const getRepositoryOptions = (options: Options) => - queryOptions>({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getRepository({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: getRepositoryQueryKey(options), - }); +export const getRepositoryOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getRepository({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getRepositoryQueryKey(options) +}); /** * Update a repository's name or settings */ -export const updateRepositoryMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await updateRepository({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const updateRepositoryMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await updateRepository({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; -export const getRepositoryStatsQueryKey = (options: Options) => - createQueryKey("getRepositoryStats", options); +export const getRepositoryStatsQueryKey = (options: Options) => createQueryKey('getRepositoryStats', options); /** * Get repository storage and compression statistics */ -export const getRepositoryStatsOptions = (options: Options) => - queryOptions< - GetRepositoryStatsResponse, - DefaultError, - GetRepositoryStatsResponse, - ReturnType - >({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getRepositoryStats({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: getRepositoryStatsQueryKey(options), - }); +export const getRepositoryStatsOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getRepositoryStats({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getRepositoryStatsQueryKey(options) +}); /** * Delete multiple snapshots from a repository */ -export const deleteSnapshotsMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await deleteSnapshots({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const deleteSnapshotsMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await deleteSnapshots({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; -export const listSnapshotsQueryKey = (options: Options) => createQueryKey("listSnapshots", options); +export const listSnapshotsQueryKey = (options: Options) => createQueryKey('listSnapshots', options); /** * List all snapshots in a repository */ -export const listSnapshotsOptions = (options: Options) => - queryOptions>({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await listSnapshots({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: listSnapshotsQueryKey(options), - }); +export const listSnapshotsOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await listSnapshots({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: listSnapshotsQueryKey(options) +}); /** * Clear snapshot cache and force refresh from repository */ -export const refreshSnapshotsMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await refreshSnapshots({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const refreshSnapshotsMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await refreshSnapshots({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; /** * Delete a specific snapshot from a repository */ -export const deleteSnapshotMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await deleteSnapshot({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const deleteSnapshotMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await deleteSnapshot({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; -export const getSnapshotDetailsQueryKey = (options: Options) => - createQueryKey("getSnapshotDetails", options); +export const getSnapshotDetailsQueryKey = (options: Options) => createQueryKey('getSnapshotDetails', options); /** * Get details of a specific snapshot */ -export const getSnapshotDetailsOptions = (options: Options) => - queryOptions< - GetSnapshotDetailsResponse, - DefaultError, - GetSnapshotDetailsResponse, - ReturnType - >({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getSnapshotDetails({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: getSnapshotDetailsQueryKey(options), - }); +export const getSnapshotDetailsOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getSnapshotDetails({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getSnapshotDetailsQueryKey(options) +}); -export const listSnapshotFilesQueryKey = (options: Options) => - createQueryKey("listSnapshotFiles", options); +export const listSnapshotFilesQueryKey = (options: Options) => createQueryKey('listSnapshotFiles', options); /** * List files and directories in a snapshot */ -export const listSnapshotFilesOptions = (options: Options) => - queryOptions< - ListSnapshotFilesResponse, - DefaultError, - ListSnapshotFilesResponse, - ReturnType - >({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await listSnapshotFiles({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: listSnapshotFilesQueryKey(options), - }); +export const listSnapshotFilesOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await listSnapshotFiles({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: listSnapshotFilesQueryKey(options) +}); -export const listSnapshotFilesInfiniteQueryKey = ( - options: Options, -): QueryKey> => createQueryKey("listSnapshotFiles", options, true); +export const listSnapshotFilesInfiniteQueryKey = (options: Options): QueryKey> => createQueryKey('listSnapshotFiles', options, true); /** * List files and directories in a snapshot */ -export const listSnapshotFilesInfiniteOptions = (options: Options) => - infiniteQueryOptions< - ListSnapshotFilesResponse, - DefaultError, - InfiniteData, - QueryKey>, - string | Pick>[0], "body" | "headers" | "path" | "query"> - >( - // @ts-ignore - { - queryFn: async ({ pageParam, queryKey, signal }) => { - // @ts-ignore - const page: Pick>[0], "body" | "headers" | "path" | "query"> = - typeof pageParam === "object" - ? pageParam - : { - query: { - offset: pageParam, - }, - }; - const params = createInfiniteParams(queryKey, page); - const { data } = await listSnapshotFiles({ - ...options, - ...params, - signal, - throwOnError: true, - }); - return data; - }, - queryKey: listSnapshotFilesInfiniteQueryKey(options), - }, - ); +export const listSnapshotFilesInfiniteOptions = (options: Options) => infiniteQueryOptions, QueryKey>, string | Pick>[0], 'body' | 'headers' | 'path' | 'query'>>( +// @ts-ignore +{ + queryFn: async ({ pageParam, queryKey, signal }) => { + // @ts-ignore + const page: Pick>[0], 'body' | 'headers' | 'path' | 'query'> = typeof pageParam === 'object' ? pageParam : { + query: { + offset: pageParam + } + }; + const params = createInfiniteParams(queryKey, page); + const { data } = await listSnapshotFiles({ + ...options, + ...params, + signal, + throwOnError: true + }); + return data; + }, + queryKey: listSnapshotFilesInfiniteQueryKey(options) +}); -export const dumpSnapshotQueryKey = (options: Options) => createQueryKey("dumpSnapshot", options); +export const dumpSnapshotQueryKey = (options: Options) => createQueryKey('dumpSnapshot', options); /** * Download a snapshot path as a tar archive (folders) or raw file stream (single files) */ -export const dumpSnapshotOptions = (options: Options) => - queryOptions>({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await dumpSnapshot({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: dumpSnapshotQueryKey(options), - }); +export const dumpSnapshotOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await dumpSnapshot({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: dumpSnapshotQueryKey(options) +}); /** * Restore a snapshot to a target path on the filesystem */ -export const restoreSnapshotMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await restoreSnapshot({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const restoreSnapshotMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await restoreSnapshot({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; /** * Cancel a running doctor operation on a repository */ -export const cancelDoctorMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await cancelDoctor({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const cancelDoctorMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await cancelDoctor({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; /** * Start an asynchronous doctor operation on a repository to fix common issues (unlock, check, repair index). The operation runs in the background and sends results via SSE events. */ -export const startDoctorMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await startDoctor({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const startDoctorMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await startDoctor({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; /** * Unlock a repository by removing all stale locks */ -export const unlockRepositoryMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await unlockRepository({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const unlockRepositoryMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await unlockRepository({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; /** * Tag multiple snapshots in a repository */ -export const tagSnapshotsMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await tagSnapshots({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const tagSnapshotsMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await tagSnapshots({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; /** * Execute a restic command against a repository (dev panel only) */ -export const devPanelExecMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await devPanelExec({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const devPanelExecMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await devPanelExec({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; -export const listBackupSchedulesQueryKey = (options?: Options) => - createQueryKey("listBackupSchedules", options); +export const listBackupSchedulesQueryKey = (options?: Options) => createQueryKey('listBackupSchedules', options); /** * List all backup schedules */ -export const listBackupSchedulesOptions = (options?: Options) => - queryOptions< - ListBackupSchedulesResponse, - DefaultError, - ListBackupSchedulesResponse, - ReturnType - >({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await listBackupSchedules({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: listBackupSchedulesQueryKey(options), - }); +export const listBackupSchedulesOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await listBackupSchedules({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: listBackupSchedulesQueryKey(options) +}); /** * Create a new backup schedule for a volume */ -export const createBackupScheduleMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions< - CreateBackupScheduleResponse, - DefaultError, - Options - > = { - mutationFn: async (fnOptions) => { - const { data } = await createBackupSchedule({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const createBackupScheduleMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await createBackupSchedule({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; /** * Delete a backup schedule */ -export const deleteBackupScheduleMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions< - DeleteBackupScheduleResponse, - DefaultError, - Options - > = { - mutationFn: async (fnOptions) => { - const { data } = await deleteBackupSchedule({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const deleteBackupScheduleMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await deleteBackupSchedule({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; -export const getBackupScheduleQueryKey = (options: Options) => - createQueryKey("getBackupSchedule", options); +export const getBackupScheduleQueryKey = (options: Options) => createQueryKey('getBackupSchedule', options); /** * Get a backup schedule by ID */ -export const getBackupScheduleOptions = (options: Options) => - queryOptions< - GetBackupScheduleResponse, - DefaultError, - GetBackupScheduleResponse, - ReturnType - >({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getBackupSchedule({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: getBackupScheduleQueryKey(options), - }); +export const getBackupScheduleOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getBackupSchedule({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getBackupScheduleQueryKey(options) +}); /** * Update a backup schedule */ -export const updateBackupScheduleMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions< - UpdateBackupScheduleResponse, - DefaultError, - Options - > = { - mutationFn: async (fnOptions) => { - const { data } = await updateBackupSchedule({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const updateBackupScheduleMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await updateBackupSchedule({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; -export const getBackupScheduleForVolumeQueryKey = (options: Options) => - createQueryKey("getBackupScheduleForVolume", options); +export const getBackupScheduleForVolumeQueryKey = (options: Options) => createQueryKey('getBackupScheduleForVolume', options); /** * Get a backup schedule for a specific volume */ -export const getBackupScheduleForVolumeOptions = (options: Options) => - queryOptions< - GetBackupScheduleForVolumeResponse, - DefaultError, - GetBackupScheduleForVolumeResponse, - ReturnType - >({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getBackupScheduleForVolume({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: getBackupScheduleForVolumeQueryKey(options), - }); +export const getBackupScheduleForVolumeOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getBackupScheduleForVolume({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getBackupScheduleForVolumeQueryKey(options) +}); /** * Trigger a backup immediately for a schedule */ -export const runBackupNowMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await runBackupNow({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const runBackupNowMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await runBackupNow({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; /** * Stop a backup that is currently in progress */ -export const stopBackupMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await stopBackup({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const stopBackupMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await stopBackup({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; /** * Manually apply retention policy to clean up old snapshots */ -export const runForgetMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await runForget({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const runForgetMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await runForget({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; -export const getScheduleNotificationsQueryKey = (options: Options) => - createQueryKey("getScheduleNotifications", options); +export const getScheduleNotificationsQueryKey = (options: Options) => createQueryKey('getScheduleNotifications', options); /** * Get notification assignments for a backup schedule */ -export const getScheduleNotificationsOptions = (options: Options) => - queryOptions< - GetScheduleNotificationsResponse, - DefaultError, - GetScheduleNotificationsResponse, - ReturnType - >({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getScheduleNotifications({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: getScheduleNotificationsQueryKey(options), - }); +export const getScheduleNotificationsOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getScheduleNotifications({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getScheduleNotificationsQueryKey(options) +}); /** * Update notification assignments for a backup schedule */ -export const updateScheduleNotificationsMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions< - UpdateScheduleNotificationsResponse, - DefaultError, - Options - > = { - mutationFn: async (fnOptions) => { - const { data } = await updateScheduleNotifications({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const updateScheduleNotificationsMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await updateScheduleNotifications({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; -export const getScheduleMirrorsQueryKey = (options: Options) => - createQueryKey("getScheduleMirrors", options); +export const getScheduleMirrorsQueryKey = (options: Options) => createQueryKey('getScheduleMirrors', options); /** * Get mirror repository assignments for a backup schedule */ -export const getScheduleMirrorsOptions = (options: Options) => - queryOptions< - GetScheduleMirrorsResponse, - DefaultError, - GetScheduleMirrorsResponse, - ReturnType - >({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getScheduleMirrors({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: getScheduleMirrorsQueryKey(options), - }); +export const getScheduleMirrorsOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getScheduleMirrors({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getScheduleMirrorsQueryKey(options) +}); /** * Update mirror repository assignments for a backup schedule */ -export const updateScheduleMirrorsMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions< - UpdateScheduleMirrorsResponse, - DefaultError, - Options - > = { - mutationFn: async (fnOptions) => { - const { data } = await updateScheduleMirrors({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const updateScheduleMirrorsMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await updateScheduleMirrors({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; -export const getMirrorCompatibilityQueryKey = (options: Options) => - createQueryKey("getMirrorCompatibility", options); +export const getMirrorCompatibilityQueryKey = (options: Options) => createQueryKey('getMirrorCompatibility', options); /** * Get mirror compatibility info for all repositories relative to a backup schedule's primary repository */ -export const getMirrorCompatibilityOptions = (options: Options) => - queryOptions< - GetMirrorCompatibilityResponse, - DefaultError, - GetMirrorCompatibilityResponse, - ReturnType - >({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getMirrorCompatibility({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: getMirrorCompatibilityQueryKey(options), - }); +export const getMirrorCompatibilityOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getMirrorCompatibility({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getMirrorCompatibilityQueryKey(options) +}); /** * Reorder backup schedules by providing an array of schedule short IDs in the desired order */ -export const reorderBackupSchedulesMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions< - ReorderBackupSchedulesResponse, - DefaultError, - Options - > = { - mutationFn: async (fnOptions) => { - const { data } = await reorderBackupSchedules({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const reorderBackupSchedulesMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await reorderBackupSchedules({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; -export const getBackupProgressQueryKey = (options: Options) => - createQueryKey("getBackupProgress", options); +export const getBackupProgressQueryKey = (options: Options) => createQueryKey('getBackupProgress', options); /** * Get the last known progress for a currently running backup. Returns null if no progress has been reported yet. */ -export const getBackupProgressOptions = (options: Options) => - queryOptions< - GetBackupProgressResponse, - DefaultError, - GetBackupProgressResponse, - ReturnType - >({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getBackupProgress({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: getBackupProgressQueryKey(options), - }); +export const getBackupProgressOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getBackupProgress({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getBackupProgressQueryKey(options) +}); -export const listNotificationDestinationsQueryKey = (options?: Options) => - createQueryKey("listNotificationDestinations", options); +export const listNotificationDestinationsQueryKey = (options?: Options) => createQueryKey('listNotificationDestinations', options); /** * List all notification destinations */ -export const listNotificationDestinationsOptions = (options?: Options) => - queryOptions< - ListNotificationDestinationsResponse, - DefaultError, - ListNotificationDestinationsResponse, - ReturnType - >({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await listNotificationDestinations({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: listNotificationDestinationsQueryKey(options), - }); +export const listNotificationDestinationsOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await listNotificationDestinations({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: listNotificationDestinationsQueryKey(options) +}); /** * Create a new notification destination */ -export const createNotificationDestinationMutation = ( - options?: Partial>, -): UseMutationOptions< - CreateNotificationDestinationResponse, - DefaultError, - Options -> => { - const mutationOptions: UseMutationOptions< - CreateNotificationDestinationResponse, - DefaultError, - Options - > = { - mutationFn: async (fnOptions) => { - const { data } = await createNotificationDestination({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const createNotificationDestinationMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await createNotificationDestination({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; /** * Delete a notification destination */ -export const deleteNotificationDestinationMutation = ( - options?: Partial>, -): UseMutationOptions< - DeleteNotificationDestinationResponse, - DefaultError, - Options -> => { - const mutationOptions: UseMutationOptions< - DeleteNotificationDestinationResponse, - DefaultError, - Options - > = { - mutationFn: async (fnOptions) => { - const { data } = await deleteNotificationDestination({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const deleteNotificationDestinationMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await deleteNotificationDestination({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; -export const getNotificationDestinationQueryKey = (options: Options) => - createQueryKey("getNotificationDestination", options); +export const getNotificationDestinationQueryKey = (options: Options) => createQueryKey('getNotificationDestination', options); /** * Get a notification destination by ID */ -export const getNotificationDestinationOptions = (options: Options) => - queryOptions< - GetNotificationDestinationResponse, - DefaultError, - GetNotificationDestinationResponse, - ReturnType - >({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getNotificationDestination({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: getNotificationDestinationQueryKey(options), - }); +export const getNotificationDestinationOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getNotificationDestination({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getNotificationDestinationQueryKey(options) +}); /** * Update a notification destination */ -export const updateNotificationDestinationMutation = ( - options?: Partial>, -): UseMutationOptions< - UpdateNotificationDestinationResponse, - DefaultError, - Options -> => { - const mutationOptions: UseMutationOptions< - UpdateNotificationDestinationResponse, - DefaultError, - Options - > = { - mutationFn: async (fnOptions) => { - const { data } = await updateNotificationDestination({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const updateNotificationDestinationMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await updateNotificationDestination({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; /** * Test a notification destination by sending a test message */ -export const testNotificationDestinationMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions< - TestNotificationDestinationResponse, - DefaultError, - Options - > = { - mutationFn: async (fnOptions) => { - const { data } = await testNotificationDestination({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const testNotificationDestinationMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await testNotificationDestination({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; -export const getSystemInfoQueryKey = (options?: Options) => createQueryKey("getSystemInfo", options); +export const getSystemInfoQueryKey = (options?: Options) => createQueryKey('getSystemInfo', options); /** * Get system information including available capabilities */ -export const getSystemInfoOptions = (options?: Options) => - queryOptions>({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getSystemInfo({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: getSystemInfoQueryKey(options), - }); +export const getSystemInfoOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getSystemInfo({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getSystemInfoQueryKey(options) +}); -export const getUpdatesQueryKey = (options?: Options) => createQueryKey("getUpdates", options); +export const getUpdatesQueryKey = (options?: Options) => createQueryKey('getUpdates', options); /** * Check for application updates from GitHub */ -export const getUpdatesOptions = (options?: Options) => - queryOptions>({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getUpdates({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: getUpdatesQueryKey(options), - }); +export const getUpdatesOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getUpdates({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getUpdatesQueryKey(options) +}); -export const getRegistrationStatusQueryKey = (options?: Options) => - createQueryKey("getRegistrationStatus", options); +export const getRegistrationStatusQueryKey = (options?: Options) => createQueryKey('getRegistrationStatus', options); /** * Get the current registration status for new users */ -export const getRegistrationStatusOptions = (options?: Options) => - queryOptions< - GetRegistrationStatusResponse, - DefaultError, - GetRegistrationStatusResponse, - ReturnType - >({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getRegistrationStatus({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: getRegistrationStatusQueryKey(options), - }); +export const getRegistrationStatusOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getRegistrationStatus({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getRegistrationStatusQueryKey(options) +}); /** * Update the registration status for new users. Requires global admin role. */ -export const setRegistrationStatusMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions< - SetRegistrationStatusResponse, - DefaultError, - Options - > = { - mutationFn: async (fnOptions) => { - const { data } = await setRegistrationStatus({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const setRegistrationStatusMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await setRegistrationStatus({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; /** * Download the organization's Restic password for backup recovery. Requires organization owner or admin role and password re-authentication. */ -export const downloadResticPasswordMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions< - DownloadResticPasswordResponse, - DefaultError, - Options - > = { - mutationFn: async (fnOptions) => { - const { data } = await downloadResticPassword({ - ...options, - ...fnOptions, - throwOnError: true, - }); - return data; - }, - }; - return mutationOptions; +export const downloadResticPasswordMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await downloadResticPassword({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; }; -export const getDevPanelQueryKey = (options?: Options) => createQueryKey("getDevPanel", options); +export const getDevPanelQueryKey = (options?: Options) => createQueryKey('getDevPanel', options); /** * Get the dev panel status */ -export const getDevPanelOptions = (options?: Options) => - queryOptions>({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getDevPanel({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: getDevPanelQueryKey(options), - }); +export const getDevPanelOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getDevPanel({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getDevPanelQueryKey(options) +}); diff --git a/app/client/api-client/client/client.gen.ts b/app/client/api-client/client/client.gen.ts index 29a4ec04..d9a078b1 100644 --- a/app/client/api-client/client/client.gen.ts +++ b/app/client/api-client/client/client.gen.ts @@ -1,286 +1,289 @@ // @ts-nocheck // This file is auto-generated by @hey-api/openapi-ts -import { createSseClient } from "../core/serverSentEvents.gen"; -import type { HttpMethod } from "../core/types.gen"; -import { getValidRequestBody } from "../core/utils.gen"; -import type { Client, Config, RequestOptions, ResolvedRequestOptions } from "./types.gen"; +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen'; import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams, -} from "./utils.gen"; + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; -type ReqInit = Omit & { - body?: any; - headers: ReturnType; +type ReqInit = Omit & { + body?: any; + headers: ReturnType; }; export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config); + let _config = mergeConfigs(createConfig(), config); - const getConfig = (): Config => ({ ..._config }); + const getConfig = (): Config => ({ ..._config }); - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config); - return getConfig(); - }; + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; - const interceptors = createInterceptors(); + const interceptors = createInterceptors(); - const beforeRequest = async (options: RequestOptions) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers), - serializedBody: undefined, - }; + const beforeRequest = async (options: RequestOptions) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined, + }; - if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); - } + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } - if (opts.requestValidator) { - await opts.requestValidator(opts); - } + if (opts.requestValidator) { + await opts.requestValidator(opts); + } - if (opts.body !== undefined && opts.bodySerializer) { - opts.serializedBody = opts.bodySerializer(opts.body); - } + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body); + } - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.serializedBody === "") { - opts.headers.delete("Content-Type"); - } + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } - const url = buildUrl(opts); + const url = buildUrl(opts); - return { opts, url }; - }; + return { opts, url }; + }; - const request: Client["request"] = async (options) => { - // @ts-expect-error - const { opts, url } = await beforeRequest(options); - const requestInit: ReqInit = { - redirect: "follow", - ...opts, - body: getValidRequestBody(opts), - }; + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options); + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: getValidRequestBody(opts), + }; - let request = new Request(url, requestInit); + let request = new Request(url, requestInit); - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); - } - } + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch!; - let response: Response; + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + let response: Response; - try { - response = await _fetch(request); - } catch (error) { - // Handle fetch exceptions (AbortError, network errors, etc.) - let finalError = error; + try { + response = await _fetch(request); + } catch (error) { + // Handle fetch exceptions (AbortError, network errors, etc.) + let finalError = error; - for (const fn of interceptors.error.fns) { - if (fn) { - finalError = (await fn(error, undefined as any, request, opts)) as unknown; - } - } + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn(error, undefined as any, request, opts)) as unknown; + } + } - finalError = finalError || ({} as unknown); + finalError = finalError || ({} as unknown); - if (opts.throwOnError) { - throw finalError; - } + if (opts.throwOnError) { + throw finalError; + } - // Return error response - return opts.responseStyle === "data" - ? undefined - : { - error: finalError, - request, - response: undefined as any, - }; - } + // Return error response + return opts.responseStyle === 'data' + ? undefined + : { + error: finalError, + request, + response: undefined as any, + }; + } - for (const fn of interceptors.response.fns) { - if (fn) { - response = await fn(response, request, opts); - } - } + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } - const result = { - request, - response, - }; + const result = { + request, + response, + }; - if (response.ok) { - const parseAs = - (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json"; + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; - if (response.status === 204 || response.headers.get("Content-Length") === "0") { - let emptyData: any; - switch (parseAs) { - case "arrayBuffer": - case "blob": - case "text": - emptyData = await response[parseAs](); - break; - case "formData": - emptyData = new FormData(); - break; - case "stream": - emptyData = response.body; - break; - case "json": - default: - emptyData = {}; - break; - } - return opts.responseStyle === "data" - ? emptyData - : { - data: emptyData, - ...result, - }; - } + if (response.status === 204 || response.headers.get('Content-Length') === '0') { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result, + }; + } - let data: any; - switch (parseAs) { - case "arrayBuffer": - case "blob": - case "formData": - case "text": - data = await response[parseAs](); - break; - case "json": { - // Some servers return 200 with no Content-Length and empty body. - // response.json() would throw; read as text and parse if non-empty. - const text = await response.text(); - data = text ? JSON.parse(text) : {}; - break; - } - case "stream": - return opts.responseStyle === "data" - ? response.body - : { - data: response.body, - ...result, - }; - } + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'text': + data = await response[parseAs](); + break; + case 'json': { + // Some servers return 200 with no Content-Length and empty body. + // response.json() would throw; read as text and parse if non-empty. + const text = await response.text(); + data = text ? JSON.parse(text) : {}; + break; + } + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } - if (parseAs === "json") { - if (opts.responseValidator) { - await opts.responseValidator(data); - } + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } - if (opts.responseTransformer) { - data = await opts.responseTransformer(data); - } - } + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } - return opts.responseStyle === "data" - ? data - : { - data, - ...result, - }; - } + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } - const textError = await response.text(); - let jsonError: unknown; + const textError = await response.text(); + let jsonError: unknown; - try { - jsonError = JSON.parse(textError); - } catch { - // noop - } + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } - const error = jsonError ?? textError; - let finalError = error; + const error = jsonError ?? textError; + let finalError = error; - for (const fn of interceptors.error.fns) { - if (fn) { - finalError = (await fn(error, response, request, opts)) as string; - } - } + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn(error, response, request, opts)) as string; + } + } - finalError = finalError || ({} as string); + finalError = finalError || ({} as string); - if (opts.throwOnError) { - throw finalError; - } + if (opts.throwOnError) { + throw finalError; + } - // TODO: we probably want to return error and improve types - return opts.responseStyle === "data" - ? undefined - : { - error: finalError, - ...result, - }; - }; + // TODO: we probably want to return error and improve types + return opts.responseStyle === 'data' + ? undefined + : { + error: finalError, + ...result, + }; + }; - const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => request({ ...options, method }); + const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); - const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { - const { opts, url } = await beforeRequest(options); - return createSseClient({ - ...opts, - body: opts.body as BodyInit | null | undefined, - headers: opts.headers as unknown as Record, - method, - onRequest: async (url, init) => { - let request = new Request(url, init); - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); - } - } - return request; - }, - serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, - url, - }); - }; + const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, + url, + }); + }; - return { - buildUrl, - connect: makeMethodFn("CONNECT"), - delete: makeMethodFn("DELETE"), - get: makeMethodFn("GET"), - getConfig, - head: makeMethodFn("HEAD"), - interceptors, - options: makeMethodFn("OPTIONS"), - patch: makeMethodFn("PATCH"), - post: makeMethodFn("POST"), - put: makeMethodFn("PUT"), - request, - setConfig, - sse: { - connect: makeSseFn("CONNECT"), - delete: makeSseFn("DELETE"), - get: makeSseFn("GET"), - head: makeSseFn("HEAD"), - options: makeSseFn("OPTIONS"), - patch: makeSseFn("PATCH"), - post: makeSseFn("POST"), - put: makeSseFn("PUT"), - trace: makeSseFn("TRACE"), - }, - trace: makeMethodFn("TRACE"), - } as Client; + return { + buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; }; diff --git a/app/client/api-client/client/types.gen.ts b/app/client/api-client/client/types.gen.ts index 430a3574..92f86372 100644 --- a/app/client/api-client/client/types.gen.ts +++ b/app/client/api-client/client/types.gen.ts @@ -1,176 +1,184 @@ // @ts-nocheck // This file is auto-generated by @hey-api/openapi-ts -import type { Auth } from "../core/auth.gen"; -import type { ServerSentEventsOptions, ServerSentEventsResult } from "../core/serverSentEvents.gen"; -import type { Client as CoreClient, Config as CoreConfig } from "../core/types.gen"; -import type { Middleware } from "./utils.gen"; +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen'; +import type { Middleware } from './utils.gen'; -export type ResponseStyle = "data" | "fields"; +export type ResponseStyle = 'data' | 'fields'; export interface Config - extends Omit, CoreConfig { - /** - * Base URL for all requests made by this client. - */ - baseUrl?: T["baseUrl"]; - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: typeof fetch; - /** - * Please don't use the Fetch client for Next.js applications. The `next` - * options won't have any effect. - * - * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. - */ - next?: never; - /** - * Return the response data parsed in a specified format. By default, `auto` - * will infer the appropriate method from the `Content-Type` response header. - * You can override this behavior with any of the {@link Body} methods. - * Select `stream` if you don't want to parse response data at all. - * - * @default 'auto' - */ - parseAs?: "arrayBuffer" | "auto" | "blob" | "formData" | "json" | "stream" | "text"; - /** - * Should we return only data or multiple fields (data, error, response, etc.)? - * - * @default 'fields' - */ - responseStyle?: ResponseStyle; - /** - * Throw an error instead of returning it in the response? - * - * @default false - */ - throwOnError?: T["throwOnError"]; + extends Omit, CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T['baseUrl']; + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Please don't use the Fetch client for Next.js applications. The `next` + * options won't have any effect. + * + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + */ + next?: never; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text'; + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; } export interface RequestOptions< - TData = unknown, - TResponseStyle extends ResponseStyle = "fields", - ThrowOnError extends boolean = boolean, - Url extends string = string, + TData = unknown, + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, > - extends - Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }>, - Pick< - ServerSentEventsOptions, - "onRequest" | "onSseError" | "onSseEvent" | "sseDefaultRetryDelay" | "sseMaxRetryAttempts" | "sseMaxRetryDelay" - > { - /** - * Any body that you want to add to your request. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} - */ - body?: unknown; - path?: Record; - query?: Record; - /** - * Security mechanism(s) to use for the request. - */ - security?: ReadonlyArray; - url: Url; + extends + Config<{ + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onRequest' + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; } export interface ResolvedRequestOptions< - TResponseStyle extends ResponseStyle = "fields", - ThrowOnError extends boolean = boolean, - Url extends string = string, + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, > extends RequestOptions { - serializedBody?: string; + serializedBody?: string; } export type RequestResult< - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = "fields", + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', > = ThrowOnError extends true - ? Promise< - TResponseStyle extends "data" - ? TData extends Record - ? TData[keyof TData] - : TData - : { - data: TData extends Record ? TData[keyof TData] : TData; - request: Request; - response: Response; - } - > - : Promise< - TResponseStyle extends "data" - ? (TData extends Record ? TData[keyof TData] : TData) | undefined - : ( - | { - data: TData extends Record ? TData[keyof TData] : TData; - error: undefined; - } - | { - data: undefined; - error: TError extends Record ? TError[keyof TError] : TError; - } - ) & { - request: Request; - response: Response; - } - >; + ? Promise< + TResponseStyle extends 'data' + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record ? TData[keyof TData] : TData; + request: Request; + response: Response; + } + > + : Promise< + TResponseStyle extends 'data' + ? (TData extends Record ? TData[keyof TData] : TData) | undefined + : ( + | { + data: TData extends Record ? TData[keyof TData] : TData; + error: undefined; + } + | { + data: undefined; + error: TError extends Record ? TError[keyof TError] : TError; + } + ) & { + request: Request; + response: Response; + } + >; export interface ClientOptions { - baseUrl?: string; - responseStyle?: ResponseStyle; - throwOnError?: boolean; + baseUrl?: string; + responseStyle?: ResponseStyle; + throwOnError?: boolean; } type MethodFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = "fields", + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, "method">, + options: Omit, 'method'>, ) => RequestResult; type SseFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = "fields", + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, "method">, + options: Omit, 'method'>, ) => Promise>; type RequestFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = "fields", + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, "method"> & - Pick>, "method">, + options: Omit, 'method'> & + Pick>, 'method'>, ) => RequestResult; type BuildUrlFn = < - TData extends { - body?: unknown; - path?: Record; - query?: Record; - url: string; - }, + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, >( - options: TData & Options, + options: TData & Options, ) => string; export type Client = CoreClient & { - interceptors: Middleware; + interceptors: Middleware; }; /** @@ -182,23 +190,26 @@ export type Client = CoreClient * to ensure your client always has the correct values. */ export type CreateClientConfig = ( - override?: Config, + override?: Config, ) => Config & T>; export interface TDataShape { - body?: unknown; - headers?: unknown; - path?: unknown; - query?: unknown; - url: string; + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; } type OmitKeys = Pick>; export type Options< - TData extends TDataShape = TDataShape, - ThrowOnError extends boolean = boolean, - TResponse = unknown, - TResponseStyle extends ResponseStyle = "fields", -> = OmitKeys, "body" | "path" | "query" | "url"> & - ([TData] extends [never] ? unknown : Omit); + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, + TResponseStyle extends ResponseStyle = 'fields', +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & + ([TData] extends [never] ? unknown : Omit); diff --git a/app/client/api-client/client/utils.gen.ts b/app/client/api-client/client/utils.gen.ts index 028f126b..e1cb1851 100644 --- a/app/client/api-client/client/utils.gen.ts +++ b/app/client/api-client/client/utils.gen.ts @@ -1,290 +1,317 @@ // @ts-nocheck // This file is auto-generated by @hey-api/openapi-ts -import { getAuthToken } from "../core/auth.gen"; -import type { QuerySerializerOptions } from "../core/bodySerializer.gen"; -import { jsonBodySerializer } from "../core/bodySerializer.gen"; -import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam } from "../core/pathSerializer.gen"; -import { getUrl } from "../core/utils.gen"; -import type { Client, ClientOptions, Config, RequestOptions } from "./types.gen"; +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; -export const createQuerySerializer = ({ parameters = {}, ...args }: QuerySerializerOptions = {}) => { - const querySerializer = (queryParams: T) => { - const search: string[] = []; - if (queryParams && typeof queryParams === "object") { - for (const name in queryParams) { - const value = queryParams[name]; +export const createQuerySerializer = ({ + parameters = {}, + ...args +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; - if (value === undefined || value === null) { - continue; - } + if (value === undefined || value === null) { + continue; + } - const options = parameters[name] || args; + const options = parameters[name] || args; - if (Array.isArray(value)) { - const serializedArray = serializeArrayParam({ - allowReserved: options.allowReserved, - explode: true, - name, - style: "form", - value, - ...options.array, - }); - if (serializedArray) search.push(serializedArray); - } else if (typeof value === "object") { - const serializedObject = serializeObjectParam({ - allowReserved: options.allowReserved, - explode: true, - name, - style: "deepObject", - value: value as Record, - ...options.object, - }); - if (serializedObject) search.push(serializedObject); - } else { - const serializedPrimitive = serializePrimitiveParam({ - allowReserved: options.allowReserved, - name, - value: value as string, - }); - if (serializedPrimitive) search.push(serializedPrimitive); - } - } - } - return search.join("&"); - }; - return querySerializer; + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'form', + value, + ...options.array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...options.object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved: options.allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; }; /** * Infers parseAs value from provided Content-Type header. */ -export const getParseAs = (contentType: string | null): Exclude => { - if (!contentType) { - // If no Content-Type header is provided, the best we can do is return the raw response body, - // which is effectively the same as the 'stream' option. - return "stream"; - } +export const getParseAs = (contentType: string | null): Exclude => { + if (!contentType) { + // If no Content-Type header is provided, the best we can do is return the raw response body, + // which is effectively the same as the 'stream' option. + return 'stream'; + } - const cleanContent = contentType.split(";")[0]?.trim(); + const cleanContent = contentType.split(';')[0]?.trim(); - if (!cleanContent) { - return; - } + if (!cleanContent) { + return; + } - if (cleanContent.startsWith("application/json") || cleanContent.endsWith("+json")) { - return "json"; - } + if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) { + return 'json'; + } - if (cleanContent === "multipart/form-data") { - return "formData"; - } + if (cleanContent === 'multipart/form-data') { + return 'formData'; + } - if (["application/", "audio/", "image/", "video/"].some((type) => cleanContent.startsWith(type))) { - return "blob"; - } + if ( + ['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type)) + ) { + return 'blob'; + } - if (cleanContent.startsWith("text/")) { - return "text"; - } + if (cleanContent.startsWith('text/')) { + return 'text'; + } - return; + return; }; const checkForExistence = ( - options: Pick & { - headers: Headers; - }, - name?: string, + options: Pick & { + headers: Headers; + }, + name?: string, ): boolean => { - if (!name) { - return false; - } - if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) { - return true; - } - return false; + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; }; export const setAuthParams = async ({ - security, - ...options -}: Pick, "security"> & - Pick & { - headers: Headers; - }) => { - for (const auth of security) { - if (checkForExistence(options, auth.name)) { - continue; - } + security, + ...options +}: Pick, 'security'> & + Pick & { + headers: Headers; + }) => { + for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } - const token = await getAuthToken(auth, options.auth); + const token = await getAuthToken(auth, options.auth); - if (!token) { - continue; - } + if (!token) { + continue; + } - const name = auth.name ?? "Authorization"; + const name = auth.name ?? 'Authorization'; - switch (auth.in) { - case "query": - if (!options.query) { - options.query = {}; - } - options.query[name] = token; - break; - case "cookie": - options.headers.append("Cookie", `${name}=${token}`); - break; - case "header": - default: - options.headers.set(name, token); - break; - } - } + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': + options.headers.append('Cookie', `${name}=${token}`); + break; + case 'header': + default: + options.headers.set(name, token); + break; + } + } }; -export const buildUrl: Client["buildUrl"] = (options) => - getUrl({ - baseUrl: options.baseUrl as string, - path: options.path, - query: options.query, - querySerializer: - typeof options.querySerializer === "function" - ? options.querySerializer - : createQuerySerializer(options.querySerializer), - url: options.url, - }); +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); export const mergeConfigs = (a: Config, b: Config): Config => { - const config = { ...a, ...b }; - if (config.baseUrl?.endsWith("/")) { - config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); - } - config.headers = mergeHeaders(a.headers, b.headers); - return config; + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith('/')) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; }; const headersEntries = (headers: Headers): Array<[string, string]> => { - const entries: Array<[string, string]> = []; - headers.forEach((value, key) => { - entries.push([key, value]); - }); - return entries; + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; }; -export const mergeHeaders = (...headers: Array["headers"] | undefined>): Headers => { - const mergedHeaders = new Headers(); - for (const header of headers) { - if (!header) { - continue; - } +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +): Headers => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header) { + continue; + } - const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); + const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); - for (const [key, value] of iterator) { - if (value === null) { - mergedHeaders.delete(key); - } else if (Array.isArray(value)) { - for (const v of value) { - mergedHeaders.append(key, v as string); - } - } else if (value !== undefined) { - // assume object headers are meant to be JSON stringified, i.e. their - // content value in OpenAPI specification is 'application/json' - mergedHeaders.set(key, typeof value === "object" ? JSON.stringify(value) : (value as string)); - } - } - } - return mergedHeaders; + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e. their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set( + key, + typeof value === 'object' ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; }; type ErrInterceptor = ( - error: Err, - response: Res, - request: Req, - options: Options, + error: Err, + response: Res, + request: Req, + options: Options, ) => Err | Promise; type ReqInterceptor = (request: Req, options: Options) => Req | Promise; -type ResInterceptor = (response: Res, request: Req, options: Options) => Res | Promise; +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; class Interceptors { - fns: Array = []; + fns: Array = []; - clear(): void { - this.fns = []; - } + clear(): void { + this.fns = []; + } - eject(id: number | Interceptor): void { - const index = this.getInterceptorIndex(id); - if (this.fns[index]) { - this.fns[index] = null; - } - } + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; + } + } - exists(id: number | Interceptor): boolean { - const index = this.getInterceptorIndex(id); - return Boolean(this.fns[index]); - } + exists(id: number | Interceptor): boolean { + const index = this.getInterceptorIndex(id); + return Boolean(this.fns[index]); + } - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === "number") { - return this.fns[id] ? id : -1; - } - return this.fns.indexOf(id); - } + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; + } + return this.fns.indexOf(id); + } - update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { - const index = this.getInterceptorIndex(id); - if (this.fns[index]) { - this.fns[index] = fn; - return id; - } - return false; - } + update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = fn; + return id; + } + return false; + } - use(fn: Interceptor): number { - this.fns.push(fn); - return this.fns.length - 1; - } + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; + } } export interface Middleware { - error: Interceptors>; - request: Interceptors>; - response: Interceptors>; + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; } -export const createInterceptors = (): Middleware => ({ - error: new Interceptors>(), - request: new Interceptors>(), - response: new Interceptors>(), +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), }); const defaultQuerySerializer = createQuerySerializer({ - allowReserved: false, - array: { - explode: true, - style: "form", - }, - object: { - explode: true, - style: "deepObject", - }, + allowReserved: false, + array: { + explode: true, + style: 'form', + }, + object: { + explode: true, + style: 'deepObject', + }, }); const defaultHeaders = { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }; export const createConfig = ( - override: Config & T> = {}, + override: Config & T> = {}, ): Config & T> => ({ - ...jsonBodySerializer, - headers: defaultHeaders, - parseAs: "auto", - querySerializer: defaultQuerySerializer, - ...override, + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: 'auto', + querySerializer: defaultQuerySerializer, + ...override, }); diff --git a/app/client/api-client/core/auth.gen.ts b/app/client/api-client/core/auth.gen.ts index 151d3864..bc564957 100644 --- a/app/client/api-client/core/auth.gen.ts +++ b/app/client/api-client/core/auth.gen.ts @@ -4,39 +4,39 @@ export type AuthToken = string | undefined; export interface Auth { - /** - * Which part of the request do we use to send the auth? - * - * @default 'header' - */ - in?: "header" | "query" | "cookie"; - /** - * Header or query parameter name. - * - * @default 'Authorization' - */ - name?: string; - scheme?: "basic" | "bearer"; - type: "apiKey" | "http"; + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; } export const getAuthToken = async ( - auth: Auth, - callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, ): Promise => { - const token = typeof callback === "function" ? await callback(auth) : callback; + const token = typeof callback === 'function' ? await callback(auth) : callback; - if (!token) { - return; - } + if (!token) { + return; + } - if (auth.scheme === "bearer") { - return `Bearer ${token}`; - } + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } - if (auth.scheme === "basic") { - return `Basic ${btoa(token)}`; - } + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } - return token; + return token; }; diff --git a/app/client/api-client/core/bodySerializer.gen.ts b/app/client/api-client/core/bodySerializer.gen.ts index 018844f0..bbfd8727 100644 --- a/app/client/api-client/core/bodySerializer.gen.ts +++ b/app/client/api-client/core/bodySerializer.gen.ts @@ -1,83 +1,85 @@ // @ts-nocheck // This file is auto-generated by @hey-api/openapi-ts -import type { ArrayStyle, ObjectStyle, SerializerOptions } from "./pathSerializer.gen"; +import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen'; export type QuerySerializer = (query: Record) => string; export type BodySerializer = (body: any) => any; type QuerySerializerOptionsObject = { - allowReserved?: boolean; - array?: Partial>; - object?: Partial>; + allowReserved?: boolean; + array?: Partial>; + object?: Partial>; }; export type QuerySerializerOptions = QuerySerializerOptionsObject & { - /** - * Per-parameter serialization overrides. When provided, these settings - * override the global array/object settings for specific parameter names. - */ - parameters?: Record; + /** + * Per-parameter serialization overrides. When provided, these settings + * override the global array/object settings for specific parameter names. + */ + parameters?: Record; }; const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { - if (typeof value === "string" || value instanceof Blob) { - data.append(key, value); - } else if (value instanceof Date) { - data.append(key, value.toISOString()); - } else { - data.append(key, JSON.stringify(value)); - } + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } }; const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { - if (typeof value === "string") { - data.append(key, value); - } else { - data.append(key, JSON.stringify(value)); - } + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } }; export const formDataBodySerializer = { - bodySerializer: | Array>>(body: T): FormData => { - const data = new FormData(); + bodySerializer: | Array>>( + body: T, + ): FormData => { + const data = new FormData(); - Object.entries(body).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeFormDataPair(data, key, v)); - } else { - serializeFormDataPair(data, key, value); - } - }); + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); - return data; - }, + return data; + }, }; export const jsonBodySerializer = { - bodySerializer: (body: T): string => - JSON.stringify(body, (_key, value) => (typeof value === "bigint" ? value.toString() : value)), + bodySerializer: (body: T): string => + JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), }; export const urlSearchParamsBodySerializer = { - bodySerializer: | Array>>(body: T): string => { - const data = new URLSearchParams(); + bodySerializer: | Array>>(body: T): string => { + const data = new URLSearchParams(); - Object.entries(body).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); - } else { - serializeUrlSearchParamsPair(data, key, value); - } - }); + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); - return data.toString(); - }, + return data.toString(); + }, }; diff --git a/app/client/api-client/core/params.gen.ts b/app/client/api-client/core/params.gen.ts index 573fd7ea..5c5cc4a3 100644 --- a/app/client/api-client/core/params.gen.ts +++ b/app/client/api-client/core/params.gen.ts @@ -1,170 +1,170 @@ // @ts-nocheck // This file is auto-generated by @hey-api/openapi-ts -type Slot = "body" | "headers" | "path" | "query"; +type Slot = 'body' | 'headers' | 'path' | 'query'; export type Field = - | { - in: Exclude; - /** - * Field name. This is the name we want the user to see and use. - */ - key: string; - /** - * Field mapped name. This is the name we want to use in the request. - * If omitted, we use the same value as `key`. - */ - map?: string; - } - | { - in: Extract; - /** - * Key isn't required for bodies. - */ - key?: string; - map?: string; - } - | { - /** - * Field name. This is the name we want the user to see and use. - */ - key: string; - /** - * Field mapped name. This is the name we want to use in the request. - * If `in` is omitted, `map` aliases `key` to the transport layer. - */ - map: Slot; - }; + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + } + | { + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If `in` is omitted, `map` aliases `key` to the transport layer. + */ + map: Slot; + }; export interface Fields { - allowExtra?: Partial>; - args?: ReadonlyArray; + allowExtra?: Partial>; + args?: ReadonlyArray; } export type FieldsConfig = ReadonlyArray; const extraPrefixesMap: Record = { - $body_: "body", - $headers_: "headers", - $path_: "path", - $query_: "query", + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', }; const extraPrefixes = Object.entries(extraPrefixesMap); type KeyMap = Map< - string, - | { - in: Slot; - map?: string; - } - | { - in?: never; - map: Slot; - } + string, + | { + in: Slot; + map?: string; + } + | { + in?: never; + map: Slot; + } >; const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { - if (!map) { - map = new Map(); - } + if (!map) { + map = new Map(); + } - for (const config of fields) { - if ("in" in config) { - if (config.key) { - map.set(config.key, { - in: config.in, - map: config.map, - }); - } - } else if ("key" in config) { - map.set(config.key, { - map: config.map, - }); - } else if (config.args) { - buildKeyMap(config.args, map); - } - } + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if ('key' in config) { + map.set(config.key, { + map: config.map, + }); + } else if (config.args) { + buildKeyMap(config.args, map); + } + } - return map; + return map; }; interface Params { - body: unknown; - headers: Record; - path: Record; - query: Record; + body: unknown; + headers: Record; + path: Record; + query: Record; } const stripEmptySlots = (params: Params) => { - for (const [slot, value] of Object.entries(params)) { - if (value && typeof value === "object" && !Object.keys(value).length) { - delete params[slot as Slot]; - } - } + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } }; export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { - const params: Params = { - body: {}, - headers: {}, - path: {}, - query: {}, - }; + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; - const map = buildKeyMap(fields); + const map = buildKeyMap(fields); - let config: FieldsConfig[number] | undefined; + let config: FieldsConfig[number] | undefined; - for (const [index, arg] of args.entries()) { - if (fields[index]) { - config = fields[index]; - } + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } - if (!config) { - continue; - } + if (!config) { + continue; + } - if ("in" in config) { - if (config.key) { - const field = map.get(config.key)!; - const name = field.map || config.key; - if (field.in) { - (params[field.in] as Record)[name] = arg; - } - } else { - params.body = arg; - } - } else { - for (const [key, value] of Object.entries(arg ?? {})) { - const field = map.get(key); + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + if (field.in) { + (params[field.in] as Record)[name] = arg; + } + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); - if (field) { - if (field.in) { - const name = field.map || key; - (params[field.in] as Record)[name] = value; - } else { - params[field.map] = value; - } - } else { - const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); + if (field) { + if (field.in) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + params[field.map] = value; + } + } else { + const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); - if (extra) { - const [prefix, slot] = extra; - (params[slot] as Record)[key.slice(prefix.length)] = value; - } else if ("allowExtra" in config && config.allowExtra) { - for (const [slot, allowed] of Object.entries(config.allowExtra)) { - if (allowed) { - (params[slot as Slot] as Record)[key] = value; - break; - } - } - } - } - } - } - } + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[key.slice(prefix.length)] = value; + } else if ('allowExtra' in config && config.allowExtra) { + for (const [slot, allowed] of Object.entries(config.allowExtra)) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } - stripEmptySlots(params); + stripEmptySlots(params); - return params; + return params; }; diff --git a/app/client/api-client/core/pathSerializer.gen.ts b/app/client/api-client/core/pathSerializer.gen.ts index 05e0a3de..075a8bc4 100644 --- a/app/client/api-client/core/pathSerializer.gen.ts +++ b/app/client/api-client/core/pathSerializer.gen.ts @@ -4,165 +4,169 @@ interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} interface SerializePrimitiveOptions { - allowReserved?: boolean; - name: string; + allowReserved?: boolean; + name: string; } export interface SerializerOptions { - /** - * @default true - */ - explode: boolean; - style: T; + /** + * @default true + */ + explode: boolean; + style: T; } -export type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited"; +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; -type MatrixStyle = "label" | "matrix" | "simple"; -export type ObjectStyle = "form" | "deepObject"; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; interface SerializePrimitiveParam extends SerializePrimitiveOptions { - value: string; + value: string; } export const separatorArrayExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case "label": - return "."; - case "matrix": - return ";"; - case "simple": - return ","; - default: - return "&"; - } + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } }; export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case "form": - return ","; - case "pipeDelimited": - return "|"; - case "spaceDelimited": - return "%20"; - default: - return ","; - } + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } }; export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { - switch (style) { - case "label": - return "."; - case "matrix": - return ";"; - case "simple": - return ","; - default: - return "&"; - } + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } }; export const serializeArrayParam = ({ - allowReserved, - explode, - name, - style, - value, + allowReserved, + explode, + name, + style, + value, }: SerializeOptions & { - value: unknown[]; + value: unknown[]; }) => { - if (!explode) { - const joinedValues = (allowReserved ? value : value.map((v) => encodeURIComponent(v as string))).join( - separatorArrayNoExplode(style), - ); - switch (style) { - case "label": - return `.${joinedValues}`; - case "matrix": - return `;${name}=${joinedValues}`; - case "simple": - return joinedValues; - default: - return `${name}=${joinedValues}`; - } - } + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } - const separator = separatorArrayExplode(style); - const joinedValues = value - .map((v) => { - if (style === "label" || style === "simple") { - return allowReserved ? v : encodeURIComponent(v as string); - } + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } - return serializePrimitiveParam({ - allowReserved, - name, - value: v as string, - }); - }) - .join(separator); - return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues; + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; }; -export const serializePrimitiveParam = ({ allowReserved, name, value }: SerializePrimitiveParam) => { - if (value === undefined || value === null) { - return ""; - } +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } - if (typeof value === "object") { - throw new Error( - "Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.", - ); - } + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } - return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; }; export const serializeObjectParam = ({ - allowReserved, - explode, - name, - style, - value, - valueOnly, + allowReserved, + explode, + name, + style, + value, + valueOnly, }: SerializeOptions & { - value: Record | Date; - valueOnly?: boolean; + value: Record | Date; + valueOnly?: boolean; }) => { - if (value instanceof Date) { - return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; - } + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } - if (style !== "deepObject" && !explode) { - let values: string[] = []; - Object.entries(value).forEach(([key, v]) => { - values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; - }); - const joinedValues = values.join(","); - switch (style) { - case "form": - return `${name}=${joinedValues}`; - case "label": - return `.${joinedValues}`; - case "matrix": - return `;${name}=${joinedValues}`; - default: - return joinedValues; - } - } + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } - const separator = separatorObjectExplode(style); - const joinedValues = Object.entries(value) - .map(([key, v]) => - serializePrimitiveParam({ - allowReserved, - name: style === "deepObject" ? `${name}[${key}]` : key, - value: v as string, - }), - ) - .join(separator); - return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues; + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; }; diff --git a/app/client/api-client/core/queryKeySerializer.gen.ts b/app/client/api-client/core/queryKeySerializer.gen.ts index 30045011..d7ba72b5 100644 --- a/app/client/api-client/core/queryKeySerializer.gen.ts +++ b/app/client/api-client/core/queryKeySerializer.gen.ts @@ -4,109 +4,115 @@ /** * JSON-friendly union that mirrors what Pinia Colada can hash. */ -export type JsonValue = null | string | number | boolean | JsonValue[] | { [key: string]: JsonValue }; +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; /** * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. */ export const queryKeyJsonReplacer = (_key: string, value: unknown) => { - if (value === undefined || typeof value === "function" || typeof value === "symbol") { - return undefined; - } - if (typeof value === "bigint") { - return value.toString(); - } - if (value instanceof Date) { - return value.toISOString(); - } - return value; + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; }; /** * Safely stringifies a value and parses it back into a JsonValue. */ export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { - try { - const json = JSON.stringify(input, queryKeyJsonReplacer); - if (json === undefined) { - return undefined; - } - return JSON.parse(json) as JsonValue; - } catch { - return undefined; - } + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } }; /** * Detects plain objects (including objects with a null prototype). */ const isPlainObject = (value: unknown): value is Record => { - if (value === null || typeof value !== "object") { - return false; - } - const prototype = Object.getPrototypeOf(value as object); - return prototype === Object.prototype || prototype === null; + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; }; /** * Turns URLSearchParams into a sorted JSON object for deterministic keys. */ const serializeSearchParams = (params: URLSearchParams): JsonValue => { - const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); - const result: Record = {}; + const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); + const result: Record = {}; - for (const [key, value] of entries) { - const existing = result[key]; - if (existing === undefined) { - result[key] = value; - continue; - } + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } - if (Array.isArray(existing)) { - (existing as string[]).push(value); - } else { - result[key] = [existing, value]; - } - } + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } - return result; + return result; }; /** * Normalizes any accepted value into a JSON-friendly shape for query keys. */ export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { - if (value === null) { - return null; - } + if (value === null) { + return null; + } - if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { - return value; - } + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return value; + } - if (value === undefined || typeof value === "function" || typeof value === "symbol") { - return undefined; - } + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } - if (typeof value === "bigint") { - return value.toString(); - } + if (typeof value === 'bigint') { + return value.toString(); + } - if (value instanceof Date) { - return value.toISOString(); - } + if (value instanceof Date) { + return value.toISOString(); + } - if (Array.isArray(value)) { - return stringifyToJsonValue(value); - } + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } - if (typeof URLSearchParams !== "undefined" && value instanceof URLSearchParams) { - return serializeSearchParams(value); - } + if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { + return serializeSearchParams(value); + } - if (isPlainObject(value)) { - return stringifyToJsonValue(value); - } + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } - return undefined; + return undefined; }; diff --git a/app/client/api-client/core/serverSentEvents.gen.ts b/app/client/api-client/core/serverSentEvents.gen.ts index cc8c8b4e..724bd1a0 100644 --- a/app/client/api-client/core/serverSentEvents.gen.ts +++ b/app/client/api-client/core/serverSentEvents.gen.ts @@ -1,240 +1,244 @@ // @ts-nocheck // This file is auto-generated by @hey-api/openapi-ts -import type { Config } from "./types.gen"; +import type { Config } from './types.gen'; -export type ServerSentEventsOptions = Omit & - Pick & { - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: typeof fetch; - /** - * Implementing clients can call request interceptors inside this hook. - */ - onRequest?: (url: string, init: RequestInit) => Promise; - /** - * Callback invoked when a network or parsing error occurs during streaming. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param error The error that occurred. - */ - onSseError?: (error: unknown) => void; - /** - * Callback invoked when an event is streamed from the server. - * - * This option applies only if the endpoint returns a stream of events. - * - * @param event Event streamed from the server. - * @returns Nothing (void). - */ - onSseEvent?: (event: StreamEvent) => void; - serializedBody?: RequestInit["body"]; - /** - * Default retry delay in milliseconds. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 3000 - */ - sseDefaultRetryDelay?: number; - /** - * Maximum number of retry attempts before giving up. - */ - sseMaxRetryAttempts?: number; - /** - * Maximum retry delay in milliseconds. - * - * Applies only when exponential backoff is used. - * - * This option applies only if the endpoint returns a stream of events. - * - * @default 30000 - */ - sseMaxRetryDelay?: number; - /** - * Optional sleep function for retry backoff. - * - * Defaults to using `setTimeout`. - */ - sseSleepFn?: (ms: number) => Promise; - url: string; - }; +export type ServerSentEventsOptions = Omit & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; export interface StreamEvent { - data: TData; - event?: string; - id?: string; - retry?: number; + data: TData; + event?: string; + id?: string; + retry?: number; } export type ServerSentEventsResult = { - stream: AsyncGenerator ? TData[keyof TData] : TData, TReturn, TNext>; + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; }; export const createSseClient = ({ - onRequest, - onSseError, - onSseEvent, - responseTransformer, - responseValidator, - sseDefaultRetryDelay, - sseMaxRetryAttempts, - sseMaxRetryDelay, - sseSleepFn, - url, - ...options + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options }: ServerSentEventsOptions): ServerSentEventsResult => { - let lastEventId: string | undefined; + let lastEventId: string | undefined; - const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); - const createStream = async function* () { - let retryDelay: number = sseDefaultRetryDelay ?? 3000; - let attempt = 0; - const signal = options.signal ?? new AbortController().signal; + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; - while (true) { - if (signal.aborted) break; + while (true) { + if (signal.aborted) break; - attempt++; + attempt++; - const headers = - options.headers instanceof Headers - ? options.headers - : new Headers(options.headers as Record | undefined); + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); - if (lastEventId !== undefined) { - headers.set("Last-Event-ID", lastEventId); - } + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } - try { - const requestInit: RequestInit = { - redirect: "follow", - ...options, - body: options.serializedBody, - headers, - signal, - }; - let request = new Request(url, requestInit); - if (onRequest) { - request = await onRequest(url, requestInit); - } - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = options.fetch ?? globalThis.fetch; - const response = await _fetch(request); + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); - if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); + if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); - if (!response.body) throw new Error("No body in SSE response"); + if (!response.body) throw new Error('No body in SSE response'); - const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); - let buffer = ""; + let buffer = ''; - const abortHandler = () => { - try { - reader.cancel(); - } catch { - // noop - } - }; + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; - signal.addEventListener("abort", abortHandler); + signal.addEventListener('abort', abortHandler); - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buffer += value; - // Normalize line endings: CRLF -> LF, then CR -> LF - buffer = buffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + // Normalize line endings: CRLF -> LF, then CR -> LF + buffer = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); - const chunks = buffer.split("\n\n"); - buffer = chunks.pop() ?? ""; + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; - for (const chunk of chunks) { - const lines = chunk.split("\n"); - const dataLines: Array = []; - let eventName: string | undefined; + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; - for (const line of lines) { - if (line.startsWith("data:")) { - dataLines.push(line.replace(/^data:\s*/, "")); - } else if (line.startsWith("event:")) { - eventName = line.replace(/^event:\s*/, ""); - } else if (line.startsWith("id:")) { - lastEventId = line.replace(/^id:\s*/, ""); - } else if (line.startsWith("retry:")) { - const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10); - if (!Number.isNaN(parsed)) { - retryDelay = parsed; - } - } - } + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } - let data: unknown; - let parsedJson = false; + let data: unknown; + let parsedJson = false; - if (dataLines.length) { - const rawData = dataLines.join("\n"); - try { - data = JSON.parse(rawData); - parsedJson = true; - } catch { - data = rawData; - } - } + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } - if (parsedJson) { - if (responseValidator) { - await responseValidator(data); - } + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } - if (responseTransformer) { - data = await responseTransformer(data); - } - } + if (responseTransformer) { + data = await responseTransformer(data); + } + } - onSseEvent?.({ - data, - event: eventName, - id: lastEventId, - retry: retryDelay, - }); + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); - if (dataLines.length) { - yield data as any; - } - } - } - } finally { - signal.removeEventListener("abort", abortHandler); - reader.releaseLock(); - } + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } - break; // exit loop on normal completion - } catch (error) { - // connection failed or aborted; retry after delay - onSseError?.(error); + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); - if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { - break; // stop after firing error - } + if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { + break; // stop after firing error + } - // exponential backoff: double retry each attempt, cap at 30s - const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); - await sleep(backoff); - } - } - }; + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); + await sleep(backoff); + } + } + }; - const stream = createStream(); + const stream = createStream(); - return { stream }; + return { stream }; }; diff --git a/app/client/api-client/core/types.gen.ts b/app/client/api-client/core/types.gen.ts index facd26d4..c6d572e5 100644 --- a/app/client/api-client/core/types.gen.ts +++ b/app/client/api-client/core/types.gen.ts @@ -1,87 +1,105 @@ // @ts-nocheck // This file is auto-generated by @hey-api/openapi-ts -import type { Auth, AuthToken } from "./auth.gen"; -import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from "./bodySerializer.gen"; +import type { Auth, AuthToken } from './auth.gen'; +import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen'; -export type HttpMethod = "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace"; +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; -export type Client = { - /** - * Returns the final request URL. - */ - buildUrl: BuildUrlFn; - getConfig: () => Config; - request: RequestFn; - setConfig: (config: Config) => Config; +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never, +> = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + getConfig: () => Config; + request: RequestFn; + setConfig: (config: Config) => Config; } & { - [K in HttpMethod]: MethodFn; + [K in HttpMethod]: MethodFn; } & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); export interface Config { - /** - * Auth token or a function returning auth token. The resolved value will be - * added to the request payload as defined by its `security` array. - */ - auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; - /** - * A function for serializing request body parameter. By default, - * {@link JSON.stringify()} will be used. - */ - bodySerializer?: BodySerializer | null; - /** - * An object containing any HTTP headers that you want to pre-populate your - * `Headers` object with. - * - * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} - */ - headers?: - | RequestInit["headers"] - | Record; - /** - * The request method. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} - */ - method?: Uppercase; - /** - * A function for serializing request query parameters. By default, arrays - * will be exploded in form style, objects will be exploded in deepObject - * style, and reserved characters are percent-encoded. - * - * This method will have no effect if the native `paramsSerializer()` Axios - * API function is used. - * - * {@link https://swagger.io/docs/specification/serialization/#query View examples} - */ - querySerializer?: QuerySerializer | QuerySerializerOptions; - /** - * A function validating request data. This is useful if you want to ensure - * the request conforms to the desired shape, so it can be safely sent to - * the server. - */ - requestValidator?: (data: unknown) => Promise; - /** - * A function transforming response data before it's returned. This is useful - * for post-processing data, e.g. converting ISO strings into Date objects. - */ - responseTransformer?: (data: unknown) => Promise; - /** - * A function validating response data. This is useful if you want to ensure - * the response conforms to the desired shape, so it can be safely passed to - * the transformers and returned to the user. - */ - responseValidator?: (data: unknown) => Promise; + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + string | number | boolean | (string | number | boolean)[] | null | undefined | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g. converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; } type IsExactlyNeverOrNeverUndefined = [T] extends [never] - ? true - : [T] extends [never | undefined] - ? [undefined] extends [T] - ? false - : true - : false; + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; export type OmitNever> = { - [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; }; diff --git a/app/client/api-client/core/utils.gen.ts b/app/client/api-client/core/utils.gen.ts index 28457f77..10452388 100644 --- a/app/client/api-client/core/utils.gen.ts +++ b/app/client/api-client/core/utils.gen.ts @@ -1,138 +1,141 @@ // @ts-nocheck // This file is auto-generated by @hey-api/openapi-ts -import type { BodySerializer, QuerySerializer } from "./bodySerializer.gen"; +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; import { - type ArraySeparatorStyle, - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from "./pathSerializer.gen"; + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; export interface PathSerializer { - path: Record; - url: string; + path: Record; + url: string; } export const PATH_PARAM_RE = /\{[^{}]+\}/g; export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = "simple"; + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; - if (name.endsWith("*")) { - explode = true; - name = name.substring(0, name.length - 1); - } + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } - if (name.startsWith(".")) { - name = name.substring(1); - style = "label"; - } else if (name.startsWith(";")) { - name = name.substring(1); - style = "matrix"; - } + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } - const value = path[name]; + const value = path[name]; - if (value === undefined || value === null) { - continue; - } + if (value === undefined || value === null) { + continue; + } - if (Array.isArray(value)) { - url = url.replace(match, serializeArrayParam({ explode, name, style, value })); - continue; - } + if (Array.isArray(value)) { + url = url.replace(match, serializeArrayParam({ explode, name, style, value })); + continue; + } - if (typeof value === "object") { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } - if (style === "matrix") { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } - const replaceValue = encodeURIComponent(style === "label" ? `.${value as string}` : (value as string)); - url = url.replace(match, replaceValue); - } - } - return url; + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; }; export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, + baseUrl, + path, + query, + querySerializer, + url: _url, }: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; }) => { - const pathUrl = _url.startsWith("/") ? _url : `/${_url}`; - let url = (baseUrl ?? "") + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ""; - if (search.startsWith("?")) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; }; export function getValidRequestBody(options: { - body?: unknown; - bodySerializer?: BodySerializer | null; - serializedBody?: unknown; + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; }) { - const hasBody = options.body !== undefined; - const isSerializedBody = hasBody && options.bodySerializer; + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; - if (isSerializedBody) { - if ("serializedBody" in options) { - const hasSerializedBody = options.serializedBody !== undefined && options.serializedBody !== ""; + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; - return hasSerializedBody ? options.serializedBody : null; - } + return hasSerializedBody ? options.serializedBody : null; + } - // not all clients implement a serializedBody property (i.e. client-axios) - return options.body !== "" ? options.body : null; - } + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } - // plain/text body - if (hasBody) { - return options.body; - } + // plain/text body + if (hasBody) { + return options.body; + } - // no body was provided - return undefined; + // no body was provided + return undefined; } diff --git a/app/client/api-client/index.ts b/app/client/api-client/index.ts index 18de4b20..9435f1ab 100644 --- a/app/client/api-client/index.ts +++ b/app/client/api-client/index.ts @@ -13,22 +13,28 @@ export { deleteRepository, deleteSnapshot, deleteSnapshots, + deleteSsoInvitation, + deleteSsoProvider, + deleteUserAccount, deleteVolume, devPanelExec, downloadResticPassword, dumpSnapshot, + getAdminUsers, getBackupProgress, getBackupSchedule, getBackupScheduleForVolume, getDevPanel, getMirrorCompatibility, getNotificationDestination, + getPublicSsoProviders, getRegistrationStatus, getRepository, getRepositoryStats, getScheduleMirrors, getScheduleNotifications, getSnapshotDetails, + getSsoSettings, getStatus, getSystemInfo, getUpdates, @@ -63,6 +69,7 @@ export { updateRepository, updateScheduleMirrors, updateScheduleNotifications, + updateSsoProviderAutoLinking, updateVolume, } from "./sdk.gen"; export type { @@ -102,6 +109,15 @@ export type { DeleteSnapshotsData, DeleteSnapshotsResponse, DeleteSnapshotsResponses, + DeleteSsoInvitationData, + DeleteSsoInvitationErrors, + DeleteSsoInvitationResponses, + DeleteSsoProviderData, + DeleteSsoProviderErrors, + DeleteSsoProviderResponses, + DeleteUserAccountData, + DeleteUserAccountErrors, + DeleteUserAccountResponses, DeleteVolumeData, DeleteVolumeResponse, DeleteVolumeResponses, @@ -115,6 +131,9 @@ export type { DumpSnapshotData, DumpSnapshotResponse, DumpSnapshotResponses, + GetAdminUsersData, + GetAdminUsersResponse, + GetAdminUsersResponses, GetBackupProgressData, GetBackupProgressResponse, GetBackupProgressResponses, @@ -134,6 +153,9 @@ export type { GetNotificationDestinationErrors, GetNotificationDestinationResponse, GetNotificationDestinationResponses, + GetPublicSsoProvidersData, + GetPublicSsoProvidersResponse, + GetPublicSsoProvidersResponses, GetRegistrationStatusData, GetRegistrationStatusResponse, GetRegistrationStatusResponses, @@ -152,6 +174,9 @@ export type { GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetSnapshotDetailsResponses, + GetSsoSettingsData, + GetSsoSettingsResponse, + GetSsoSettingsResponses, GetStatusData, GetStatusResponse, GetStatusResponses, @@ -258,6 +283,9 @@ export type { UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateScheduleNotificationsResponses, + UpdateSsoProviderAutoLinkingData, + UpdateSsoProviderAutoLinkingErrors, + UpdateSsoProviderAutoLinkingResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponse, diff --git a/app/client/api-client/sdk.gen.ts b/app/client/api-client/sdk.gen.ts index 247e80de..21199845 100644 --- a/app/client/api-client/sdk.gen.ts +++ b/app/client/api-client/sdk.gen.ts @@ -1,868 +1,493 @@ // @ts-nocheck // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Options as Options2, TDataShape } from "./client"; -import { client } from "./client.gen"; -import type { - BrowseFilesystemData, - BrowseFilesystemResponses, - CancelDoctorData, - CancelDoctorErrors, - CancelDoctorResponses, - CreateBackupScheduleData, - CreateBackupScheduleResponses, - CreateNotificationDestinationData, - CreateNotificationDestinationResponses, - CreateRepositoryData, - CreateRepositoryResponses, - CreateVolumeData, - CreateVolumeResponses, - DeleteBackupScheduleData, - DeleteBackupScheduleResponses, - DeleteNotificationDestinationData, - DeleteNotificationDestinationErrors, - DeleteNotificationDestinationResponses, - DeleteRepositoryData, - DeleteRepositoryResponses, - DeleteSnapshotData, - DeleteSnapshotResponses, - DeleteSnapshotsData, - DeleteSnapshotsResponses, - DeleteVolumeData, - DeleteVolumeResponses, - DevPanelExecData, - DevPanelExecErrors, - DevPanelExecResponses, - DownloadResticPasswordData, - DownloadResticPasswordResponses, - DumpSnapshotData, - DumpSnapshotResponses, - GetBackupProgressData, - GetBackupProgressResponses, - GetBackupScheduleData, - GetBackupScheduleForVolumeData, - GetBackupScheduleForVolumeResponses, - GetBackupScheduleResponses, - GetDevPanelData, - GetDevPanelResponses, - GetMirrorCompatibilityData, - GetMirrorCompatibilityResponses, - GetNotificationDestinationData, - GetNotificationDestinationErrors, - GetNotificationDestinationResponses, - GetRegistrationStatusData, - GetRegistrationStatusResponses, - GetRepositoryData, - GetRepositoryResponses, - GetRepositoryStatsData, - GetRepositoryStatsResponses, - GetScheduleMirrorsData, - GetScheduleMirrorsResponses, - GetScheduleNotificationsData, - GetScheduleNotificationsResponses, - GetSnapshotDetailsData, - GetSnapshotDetailsResponses, - GetStatusData, - GetStatusResponses, - GetSystemInfoData, - GetSystemInfoResponses, - GetUpdatesData, - GetUpdatesResponses, - GetUserDeletionImpactData, - GetUserDeletionImpactResponses, - GetVolumeData, - GetVolumeErrors, - GetVolumeResponses, - HealthCheckVolumeData, - HealthCheckVolumeErrors, - HealthCheckVolumeResponses, - ListBackupSchedulesData, - ListBackupSchedulesResponses, - ListFilesData, - ListFilesResponses, - ListNotificationDestinationsData, - ListNotificationDestinationsResponses, - ListRcloneRemotesData, - ListRcloneRemotesResponses, - ListRepositoriesData, - ListRepositoriesResponses, - ListSnapshotFilesData, - ListSnapshotFilesResponses, - ListSnapshotsData, - ListSnapshotsResponses, - ListVolumesData, - ListVolumesResponses, - MountVolumeData, - MountVolumeResponses, - RefreshSnapshotsData, - RefreshSnapshotsResponses, - ReorderBackupSchedulesData, - ReorderBackupSchedulesResponses, - RestoreSnapshotData, - RestoreSnapshotResponses, - RunBackupNowData, - RunBackupNowResponses, - RunForgetData, - RunForgetResponses, - SetRegistrationStatusData, - SetRegistrationStatusResponses, - StartDoctorData, - StartDoctorErrors, - StartDoctorResponses, - StopBackupData, - StopBackupErrors, - StopBackupResponses, - TagSnapshotsData, - TagSnapshotsResponses, - TestConnectionData, - TestConnectionResponses, - TestNotificationDestinationData, - TestNotificationDestinationErrors, - TestNotificationDestinationResponses, - UnlockRepositoryData, - UnlockRepositoryResponses, - UnmountVolumeData, - UnmountVolumeResponses, - UpdateBackupScheduleData, - UpdateBackupScheduleResponses, - UpdateNotificationDestinationData, - UpdateNotificationDestinationErrors, - UpdateNotificationDestinationResponses, - UpdateRepositoryData, - UpdateRepositoryErrors, - UpdateRepositoryResponses, - UpdateScheduleMirrorsData, - UpdateScheduleMirrorsResponses, - UpdateScheduleNotificationsData, - UpdateScheduleNotificationsResponses, - UpdateVolumeData, - UpdateVolumeErrors, - UpdateVolumeResponses, -} from "./types.gen"; +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; +import type { BrowseFilesystemData, BrowseFilesystemResponses, CancelDoctorData, CancelDoctorErrors, CancelDoctorResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteSnapshotsData, DeleteSnapshotsResponses, DeleteSsoInvitationData, DeleteSsoInvitationErrors, DeleteSsoInvitationResponses, DeleteSsoProviderData, DeleteSsoProviderErrors, DeleteSsoProviderResponses, DeleteUserAccountData, DeleteUserAccountErrors, DeleteUserAccountResponses, DeleteVolumeData, DeleteVolumeResponses, DevPanelExecData, DevPanelExecErrors, DevPanelExecResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, DumpSnapshotData, DumpSnapshotResponses, GetAdminUsersData, GetAdminUsersResponses, GetBackupProgressData, GetBackupProgressResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetDevPanelData, GetDevPanelResponses, GetMirrorCompatibilityData, GetMirrorCompatibilityResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetPublicSsoProvidersData, GetPublicSsoProvidersResponses, GetRegistrationStatusData, GetRegistrationStatusResponses, GetRepositoryData, GetRepositoryResponses, GetRepositoryStatsData, GetRepositoryStatsResponses, GetScheduleMirrorsData, GetScheduleMirrorsResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetSsoSettingsData, GetSsoSettingsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetUpdatesData, GetUpdatesResponses, GetUserDeletionImpactData, GetUserDeletionImpactResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListNotificationDestinationsData, ListNotificationDestinationsResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, MountVolumeData, MountVolumeResponses, RefreshSnapshotsData, RefreshSnapshotsResponses, ReorderBackupSchedulesData, ReorderBackupSchedulesResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, SetRegistrationStatusData, SetRegistrationStatusResponses, StartDoctorData, StartDoctorErrors, StartDoctorResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TagSnapshotsData, TagSnapshotsResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnlockRepositoryData, UnlockRepositoryResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateSsoProviderAutoLinkingData, UpdateSsoProviderAutoLinkingErrors, UpdateSsoProviderAutoLinkingResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen'; -export type Options = Options2< - TData, - ThrowOnError -> & { - /** - * You can provide a client instance returned by `createClient()` instead of - * individual options. This might be also useful if you want to implement a - * custom client. - */ - client?: Client; - /** - * You can pass arbitrary values through the `meta` object. This can be - * used to access values that aren't defined as part of the SDK function. - */ - meta?: Record; +export type Options = Options2 & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client; + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record; }; /** * Get authentication system status */ -export const getStatus = (options?: Options) => - (options?.client ?? client).get({ - url: "/api/v1/auth/status", - ...options, - }); +export const getStatus = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/auth/status', ...options }); + +/** + * Get public SSO providers for the instance + */ +export const getPublicSsoProviders = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/auth/sso-providers', ...options }); + +/** + * Get SSO providers and invitations for the active organization + */ +export const getSsoSettings = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/auth/sso-settings', ...options }); + +/** + * Delete an SSO provider + */ +export const deleteSsoProvider = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/auth/sso-providers/{providerId}', ...options }); + +/** + * Update whether SSO sign-in can auto-link existing accounts by email + */ +export const updateSsoProviderAutoLinking = (options: Options) => (options.client ?? client).patch({ + url: '/api/v1/auth/sso-providers/{providerId}/auto-linking', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Delete an SSO invitation + */ +export const deleteSsoInvitation = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/auth/sso-invitations/{invitationId}', ...options }); + +/** + * List admin users for settings management + */ +export const getAdminUsers = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/auth/admin-users', ...options }); + +/** + * Delete an account linked to a user + */ +export const deleteUserAccount = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/auth/admin-users/{userId}/accounts/{accountId}', ...options }); /** * Get impact of deleting a user */ -export const getUserDeletionImpact = ( - options: Options, -) => - (options.client ?? client).get({ - url: "/api/v1/auth/deletion-impact/{userId}", - ...options, - }); +export const getUserDeletionImpact = (options: Options) => (options.client ?? client).get({ url: '/api/v1/auth/deletion-impact/{userId}', ...options }); /** * List all volumes */ -export const listVolumes = (options?: Options) => - (options?.client ?? client).get({ url: "/api/v1/volumes", ...options }); +export const listVolumes = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/volumes', ...options }); /** * Create a new volume */ -export const createVolume = (options?: Options) => - (options?.client ?? client).post({ - url: "/api/v1/volumes", - ...options, - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, - }); +export const createVolume = (options?: Options) => (options?.client ?? client).post({ + url: '/api/v1/volumes', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } +}); /** * Test connection to backend */ -export const testConnection = ( - options?: Options, -) => - (options?.client ?? client).post({ - url: "/api/v1/volumes/test-connection", - ...options, - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, - }); +export const testConnection = (options?: Options) => (options?.client ?? client).post({ + url: '/api/v1/volumes/test-connection', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } +}); /** * Delete a volume */ -export const deleteVolume = (options: Options) => - (options.client ?? client).delete({ - url: "/api/v1/volumes/{shortId}", - ...options, - }); +export const deleteVolume = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/volumes/{shortId}', ...options }); /** * Get a volume by name */ -export const getVolume = (options: Options) => - (options.client ?? client).get({ - url: "/api/v1/volumes/{shortId}", - ...options, - }); +export const getVolume = (options: Options) => (options.client ?? client).get({ url: '/api/v1/volumes/{shortId}', ...options }); /** * Update a volume's configuration */ -export const updateVolume = (options: Options) => - (options.client ?? client).put({ - url: "/api/v1/volumes/{shortId}", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }); +export const updateVolume = (options: Options) => (options.client ?? client).put({ + url: '/api/v1/volumes/{shortId}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); /** * Mount a volume */ -export const mountVolume = (options: Options) => - (options.client ?? client).post({ - url: "/api/v1/volumes/{shortId}/mount", - ...options, - }); +export const mountVolume = (options: Options) => (options.client ?? client).post({ url: '/api/v1/volumes/{shortId}/mount', ...options }); /** * Unmount a volume */ -export const unmountVolume = ( - options: Options, -) => - (options.client ?? client).post({ - url: "/api/v1/volumes/{shortId}/unmount", - ...options, - }); +export const unmountVolume = (options: Options) => (options.client ?? client).post({ url: '/api/v1/volumes/{shortId}/unmount', ...options }); /** * Perform a health check on a volume */ -export const healthCheckVolume = ( - options: Options, -) => - (options.client ?? client).post({ - url: "/api/v1/volumes/{shortId}/health-check", - ...options, - }); +export const healthCheckVolume = (options: Options) => (options.client ?? client).post({ url: '/api/v1/volumes/{shortId}/health-check', ...options }); /** * List files in a volume directory */ -export const listFiles = (options: Options) => - (options.client ?? client).get({ - url: "/api/v1/volumes/{shortId}/files", - ...options, - }); +export const listFiles = (options: Options) => (options.client ?? client).get({ url: '/api/v1/volumes/{shortId}/files', ...options }); /** * Browse directories on the host filesystem */ -export const browseFilesystem = ( - options?: Options, -) => - (options?.client ?? client).get({ - url: "/api/v1/volumes/filesystem/browse", - ...options, - }); +export const browseFilesystem = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/volumes/filesystem/browse', ...options }); /** * List all repositories */ -export const listRepositories = ( - options?: Options, -) => - (options?.client ?? client).get({ - url: "/api/v1/repositories", - ...options, - }); +export const listRepositories = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/repositories', ...options }); /** * Create a new restic repository */ -export const createRepository = ( - options?: Options, -) => - (options?.client ?? client).post({ - url: "/api/v1/repositories", - ...options, - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, - }); +export const createRepository = (options?: Options) => (options?.client ?? client).post({ + url: '/api/v1/repositories', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } +}); /** * List all configured rclone remotes on the host system */ -export const listRcloneRemotes = ( - options?: Options, -) => - (options?.client ?? client).get({ - url: "/api/v1/repositories/rclone-remotes", - ...options, - }); +export const listRcloneRemotes = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/repositories/rclone-remotes', ...options }); /** * Delete a repository */ -export const deleteRepository = ( - options: Options, -) => - (options.client ?? client).delete({ - url: "/api/v1/repositories/{shortId}", - ...options, - }); +export const deleteRepository = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/repositories/{shortId}', ...options }); /** * Get a single repository by ID */ -export const getRepository = ( - options: Options, -) => - (options.client ?? client).get({ - url: "/api/v1/repositories/{shortId}", - ...options, - }); +export const getRepository = (options: Options) => (options.client ?? client).get({ url: '/api/v1/repositories/{shortId}', ...options }); /** * Update a repository's name or settings */ -export const updateRepository = ( - options: Options, -) => - (options.client ?? client).patch({ - url: "/api/v1/repositories/{shortId}", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }); +export const updateRepository = (options: Options) => (options.client ?? client).patch({ + url: '/api/v1/repositories/{shortId}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); /** * Get repository storage and compression statistics */ -export const getRepositoryStats = ( - options: Options, -) => - (options.client ?? client).get({ - url: "/api/v1/repositories/{shortId}/stats", - ...options, - }); +export const getRepositoryStats = (options: Options) => (options.client ?? client).get({ url: '/api/v1/repositories/{shortId}/stats', ...options }); /** * Delete multiple snapshots from a repository */ -export const deleteSnapshots = ( - options: Options, -) => - (options.client ?? client).delete({ - url: "/api/v1/repositories/{shortId}/snapshots", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }); +export const deleteSnapshots = (options: Options) => (options.client ?? client).delete({ + url: '/api/v1/repositories/{shortId}/snapshots', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); /** * List all snapshots in a repository */ -export const listSnapshots = ( - options: Options, -) => - (options.client ?? client).get({ - url: "/api/v1/repositories/{shortId}/snapshots", - ...options, - }); +export const listSnapshots = (options: Options) => (options.client ?? client).get({ url: '/api/v1/repositories/{shortId}/snapshots', ...options }); /** * Clear snapshot cache and force refresh from repository */ -export const refreshSnapshots = ( - options: Options, -) => - (options.client ?? client).post({ - url: "/api/v1/repositories/{shortId}/snapshots/refresh", - ...options, - }); +export const refreshSnapshots = (options: Options) => (options.client ?? client).post({ url: '/api/v1/repositories/{shortId}/snapshots/refresh', ...options }); /** * Delete a specific snapshot from a repository */ -export const deleteSnapshot = ( - options: Options, -) => - (options.client ?? client).delete({ - url: "/api/v1/repositories/{shortId}/snapshots/{snapshotId}", - ...options, - }); +export const deleteSnapshot = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}', ...options }); /** * Get details of a specific snapshot */ -export const getSnapshotDetails = ( - options: Options, -) => - (options.client ?? client).get({ - url: "/api/v1/repositories/{shortId}/snapshots/{snapshotId}", - ...options, - }); +export const getSnapshotDetails = (options: Options) => (options.client ?? client).get({ url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}', ...options }); /** * List files and directories in a snapshot */ -export const listSnapshotFiles = ( - options: Options, -) => - (options.client ?? client).get({ - url: "/api/v1/repositories/{shortId}/snapshots/{snapshotId}/files", - ...options, - }); +export const listSnapshotFiles = (options: Options) => (options.client ?? client).get({ url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}/files', ...options }); /** * Download a snapshot path as a tar archive (folders) or raw file stream (single files) */ -export const dumpSnapshot = (options: Options) => - (options.client ?? client).get({ - url: "/api/v1/repositories/{shortId}/snapshots/{snapshotId}/dump", - ...options, - }); +export const dumpSnapshot = (options: Options) => (options.client ?? client).get({ url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}/dump', ...options }); /** * Restore a snapshot to a target path on the filesystem */ -export const restoreSnapshot = ( - options: Options, -) => - (options.client ?? client).post({ - url: "/api/v1/repositories/{shortId}/restore", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }); +export const restoreSnapshot = (options: Options) => (options.client ?? client).post({ + url: '/api/v1/repositories/{shortId}/restore', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); /** * Cancel a running doctor operation on a repository */ -export const cancelDoctor = (options: Options) => - (options.client ?? client).delete({ - url: "/api/v1/repositories/{shortId}/doctor", - ...options, - }); +export const cancelDoctor = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/repositories/{shortId}/doctor', ...options }); /** * Start an asynchronous doctor operation on a repository to fix common issues (unlock, check, repair index). The operation runs in the background and sends results via SSE events. */ -export const startDoctor = (options: Options) => - (options.client ?? client).post({ - url: "/api/v1/repositories/{shortId}/doctor", - ...options, - }); +export const startDoctor = (options: Options) => (options.client ?? client).post({ url: '/api/v1/repositories/{shortId}/doctor', ...options }); /** * Unlock a repository by removing all stale locks */ -export const unlockRepository = ( - options: Options, -) => - (options.client ?? client).post({ - url: "/api/v1/repositories/{shortId}/unlock", - ...options, - }); +export const unlockRepository = (options: Options) => (options.client ?? client).post({ url: '/api/v1/repositories/{shortId}/unlock', ...options }); /** * Tag multiple snapshots in a repository */ -export const tagSnapshots = (options: Options) => - (options.client ?? client).post({ - url: "/api/v1/repositories/{shortId}/snapshots/tag", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }); +export const tagSnapshots = (options: Options) => (options.client ?? client).post({ + url: '/api/v1/repositories/{shortId}/snapshots/tag', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); /** * Execute a restic command against a repository (dev panel only) */ -export const devPanelExec = (options: Options) => - (options.client ?? client).sse.post({ - url: "/api/v1/repositories/{shortId}/exec", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }); +export const devPanelExec = (options: Options) => (options.client ?? client).sse.post({ + url: '/api/v1/repositories/{shortId}/exec', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); /** * List all backup schedules */ -export const listBackupSchedules = ( - options?: Options, -) => - (options?.client ?? client).get({ - url: "/api/v1/backups", - ...options, - }); +export const listBackupSchedules = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/backups', ...options }); /** * Create a new backup schedule for a volume */ -export const createBackupSchedule = ( - options?: Options, -) => - (options?.client ?? client).post({ - url: "/api/v1/backups", - ...options, - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, - }); +export const createBackupSchedule = (options?: Options) => (options?.client ?? client).post({ + url: '/api/v1/backups', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } +}); /** * Delete a backup schedule */ -export const deleteBackupSchedule = ( - options: Options, -) => - (options.client ?? client).delete({ - url: "/api/v1/backups/{shortId}", - ...options, - }); +export const deleteBackupSchedule = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/backups/{shortId}', ...options }); /** * Get a backup schedule by ID */ -export const getBackupSchedule = ( - options: Options, -) => - (options.client ?? client).get({ - url: "/api/v1/backups/{shortId}", - ...options, - }); +export const getBackupSchedule = (options: Options) => (options.client ?? client).get({ url: '/api/v1/backups/{shortId}', ...options }); /** * Update a backup schedule */ -export const updateBackupSchedule = ( - options: Options, -) => - (options.client ?? client).patch({ - url: "/api/v1/backups/{shortId}", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }); +export const updateBackupSchedule = (options: Options) => (options.client ?? client).patch({ + url: '/api/v1/backups/{shortId}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); /** * Get a backup schedule for a specific volume */ -export const getBackupScheduleForVolume = ( - options: Options, -) => - (options.client ?? client).get({ - url: "/api/v1/backups/volume/{volumeShortId}", - ...options, - }); +export const getBackupScheduleForVolume = (options: Options) => (options.client ?? client).get({ url: '/api/v1/backups/volume/{volumeShortId}', ...options }); /** * Trigger a backup immediately for a schedule */ -export const runBackupNow = (options: Options) => - (options.client ?? client).post({ - url: "/api/v1/backups/{shortId}/run", - ...options, - }); +export const runBackupNow = (options: Options) => (options.client ?? client).post({ url: '/api/v1/backups/{shortId}/run', ...options }); /** * Stop a backup that is currently in progress */ -export const stopBackup = (options: Options) => - (options.client ?? client).post({ - url: "/api/v1/backups/{shortId}/stop", - ...options, - }); +export const stopBackup = (options: Options) => (options.client ?? client).post({ url: '/api/v1/backups/{shortId}/stop', ...options }); /** * Manually apply retention policy to clean up old snapshots */ -export const runForget = (options: Options) => - (options.client ?? client).post({ - url: "/api/v1/backups/{shortId}/forget", - ...options, - }); +export const runForget = (options: Options) => (options.client ?? client).post({ url: '/api/v1/backups/{shortId}/forget', ...options }); /** * Get notification assignments for a backup schedule */ -export const getScheduleNotifications = ( - options: Options, -) => - (options.client ?? client).get({ - url: "/api/v1/backups/{shortId}/notifications", - ...options, - }); +export const getScheduleNotifications = (options: Options) => (options.client ?? client).get({ url: '/api/v1/backups/{shortId}/notifications', ...options }); /** * Update notification assignments for a backup schedule */ -export const updateScheduleNotifications = ( - options: Options, -) => - (options.client ?? client).put({ - url: "/api/v1/backups/{shortId}/notifications", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }); +export const updateScheduleNotifications = (options: Options) => (options.client ?? client).put({ + url: '/api/v1/backups/{shortId}/notifications', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); /** * Get mirror repository assignments for a backup schedule */ -export const getScheduleMirrors = ( - options: Options, -) => - (options.client ?? client).get({ - url: "/api/v1/backups/{shortId}/mirrors", - ...options, - }); +export const getScheduleMirrors = (options: Options) => (options.client ?? client).get({ url: '/api/v1/backups/{shortId}/mirrors', ...options }); /** * Update mirror repository assignments for a backup schedule */ -export const updateScheduleMirrors = ( - options: Options, -) => - (options.client ?? client).put({ - url: "/api/v1/backups/{shortId}/mirrors", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }); +export const updateScheduleMirrors = (options: Options) => (options.client ?? client).put({ + url: '/api/v1/backups/{shortId}/mirrors', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); /** * Get mirror compatibility info for all repositories relative to a backup schedule's primary repository */ -export const getMirrorCompatibility = ( - options: Options, -) => - (options.client ?? client).get({ - url: "/api/v1/backups/{shortId}/mirrors/compatibility", - ...options, - }); +export const getMirrorCompatibility = (options: Options) => (options.client ?? client).get({ url: '/api/v1/backups/{shortId}/mirrors/compatibility', ...options }); /** * Reorder backup schedules by providing an array of schedule short IDs in the desired order */ -export const reorderBackupSchedules = ( - options?: Options, -) => - (options?.client ?? client).post({ - url: "/api/v1/backups/reorder", - ...options, - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, - }); +export const reorderBackupSchedules = (options?: Options) => (options?.client ?? client).post({ + url: '/api/v1/backups/reorder', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } +}); /** * Get the last known progress for a currently running backup. Returns null if no progress has been reported yet. */ -export const getBackupProgress = ( - options: Options, -) => - (options.client ?? client).get({ - url: "/api/v1/backups/{shortId}/progress", - ...options, - }); +export const getBackupProgress = (options: Options) => (options.client ?? client).get({ url: '/api/v1/backups/{shortId}/progress', ...options }); /** * List all notification destinations */ -export const listNotificationDestinations = ( - options?: Options, -) => - (options?.client ?? client).get({ - url: "/api/v1/notifications/destinations", - ...options, - }); +export const listNotificationDestinations = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/notifications/destinations', ...options }); /** * Create a new notification destination */ -export const createNotificationDestination = ( - options?: Options, -) => - (options?.client ?? client).post({ - url: "/api/v1/notifications/destinations", - ...options, - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, - }); +export const createNotificationDestination = (options?: Options) => (options?.client ?? client).post({ + url: '/api/v1/notifications/destinations', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } +}); /** * Delete a notification destination */ -export const deleteNotificationDestination = ( - options: Options, -) => - (options.client ?? client).delete< - DeleteNotificationDestinationResponses, - DeleteNotificationDestinationErrors, - ThrowOnError - >({ url: "/api/v1/notifications/destinations/{id}", ...options }); +export const deleteNotificationDestination = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/notifications/destinations/{id}', ...options }); /** * Get a notification destination by ID */ -export const getNotificationDestination = ( - options: Options, -) => - (options.client ?? client).get({ - url: "/api/v1/notifications/destinations/{id}", - ...options, - }); +export const getNotificationDestination = (options: Options) => (options.client ?? client).get({ url: '/api/v1/notifications/destinations/{id}', ...options }); /** * Update a notification destination */ -export const updateNotificationDestination = ( - options: Options, -) => - (options.client ?? client).patch< - UpdateNotificationDestinationResponses, - UpdateNotificationDestinationErrors, - ThrowOnError - >({ - url: "/api/v1/notifications/destinations/{id}", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, - }); +export const updateNotificationDestination = (options: Options) => (options.client ?? client).patch({ + url: '/api/v1/notifications/destinations/{id}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); /** * Test a notification destination by sending a test message */ -export const testNotificationDestination = ( - options: Options, -) => - (options.client ?? client).post< - TestNotificationDestinationResponses, - TestNotificationDestinationErrors, - ThrowOnError - >({ url: "/api/v1/notifications/destinations/{id}/test", ...options }); +export const testNotificationDestination = (options: Options) => (options.client ?? client).post({ url: '/api/v1/notifications/destinations/{id}/test', ...options }); /** * Get system information including available capabilities */ -export const getSystemInfo = ( - options?: Options, -) => - (options?.client ?? client).get({ - url: "/api/v1/system/info", - ...options, - }); +export const getSystemInfo = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/system/info', ...options }); /** * Check for application updates from GitHub */ -export const getUpdates = (options?: Options) => - (options?.client ?? client).get({ - url: "/api/v1/system/updates", - ...options, - }); +export const getUpdates = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/system/updates', ...options }); /** * Get the current registration status for new users */ -export const getRegistrationStatus = ( - options?: Options, -) => - (options?.client ?? client).get({ - url: "/api/v1/system/registration-status", - ...options, - }); +export const getRegistrationStatus = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/system/registration-status', ...options }); /** * Update the registration status for new users. Requires global admin role. */ -export const setRegistrationStatus = ( - options?: Options, -) => - (options?.client ?? client).put({ - url: "/api/v1/system/registration-status", - ...options, - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, - }); +export const setRegistrationStatus = (options?: Options) => (options?.client ?? client).put({ + url: '/api/v1/system/registration-status', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } +}); /** * Download the organization's Restic password for backup recovery. Requires organization owner or admin role and password re-authentication. */ -export const downloadResticPassword = ( - options?: Options, -) => - (options?.client ?? client).post({ - url: "/api/v1/system/restic-password", - ...options, - headers: { - "Content-Type": "application/json", - ...options?.headers, - }, - }); +export const downloadResticPassword = (options?: Options) => (options?.client ?? client).post({ + url: '/api/v1/system/restic-password', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } +}); /** * Get the dev panel status */ -export const getDevPanel = (options?: Options) => - (options?.client ?? client).get({ - url: "/api/v1/system/dev-panel", - ...options, - }); +export const getDevPanel = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/system/dev-panel', ...options }); diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index 1ead72fd..c3bb01dc 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -2,4725 +2,4693 @@ // This file is auto-generated by @hey-api/openapi-ts export type ClientOptions = { - baseUrl: "http://localhost:4096" | (string & {}); + baseUrl: 'http://localhost:4096' | (string & {}); }; export type GetStatusData = { - body?: never; - path?: never; - query?: never; - url: "/api/v1/auth/status"; + body?: never; + path?: never; + query?: never; + url: '/api/v1/auth/status'; }; export type GetStatusResponses = { - /** - * Authentication system status - */ - 200: { - hasUsers: boolean; - }; + /** + * Authentication system status + */ + 200: { + hasUsers: boolean; + }; }; export type GetStatusResponse = GetStatusResponses[keyof GetStatusResponses]; +export type GetPublicSsoProvidersData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/auth/sso-providers'; +}; + +export type GetPublicSsoProvidersResponses = { + /** + * List of public SSO providers + */ + 200: { + providers: Array<{ + organizationSlug: string; + providerId: string; + }>; + }; +}; + +export type GetPublicSsoProvidersResponse = GetPublicSsoProvidersResponses[keyof GetPublicSsoProvidersResponses]; + +export type GetSsoSettingsData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/auth/sso-settings'; +}; + +export type GetSsoSettingsResponses = { + /** + * SSO settings for the active organization + */ + 200: { + invitations: Array<{ + email: string; + expiresAt: string; + id: string; + role: string; + status: string; + }>; + providers: Array<{ + autoLinkMatchingEmails: boolean; + domain: string; + issuer: string; + organizationId: string | null; + providerId: string; + type: string; + }>; + }; +}; + +export type GetSsoSettingsResponse = GetSsoSettingsResponses[keyof GetSsoSettingsResponses]; + +export type DeleteSsoProviderData = { + body?: never; + path: { + providerId: string; + }; + query?: never; + url: '/api/v1/auth/sso-providers/{providerId}'; +}; + +export type DeleteSsoProviderErrors = { + /** + * Forbidden + */ + 403: unknown; + /** + * Provider not found + */ + 404: unknown; +}; + +export type DeleteSsoProviderResponses = { + /** + * SSO provider deleted successfully + */ + 200: unknown; +}; + +export type UpdateSsoProviderAutoLinkingData = { + body?: { + enabled: boolean; + }; + path: { + providerId: string; + }; + query?: never; + url: '/api/v1/auth/sso-providers/{providerId}/auto-linking'; +}; + +export type UpdateSsoProviderAutoLinkingErrors = { + /** + * Forbidden + */ + 403: unknown; + /** + * Provider not found + */ + 404: unknown; +}; + +export type UpdateSsoProviderAutoLinkingResponses = { + /** + * SSO provider auto-linking setting updated successfully + */ + 200: unknown; +}; + +export type DeleteSsoInvitationData = { + body?: never; + path: { + invitationId: string; + }; + query?: never; + url: '/api/v1/auth/sso-invitations/{invitationId}'; +}; + +export type DeleteSsoInvitationErrors = { + /** + * Forbidden + */ + 403: unknown; +}; + +export type DeleteSsoInvitationResponses = { + /** + * SSO invitation deleted successfully + */ + 200: unknown; +}; + +export type GetAdminUsersData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/auth/admin-users'; +}; + +export type GetAdminUsersResponses = { + /** + * List of users with roles and status + */ + 200: { + total: number; + users: Array<{ + accounts: Array<{ + id: string; + providerId: string; + }>; + banned: boolean; + email: string; + id: string; + name: string | null; + role: string; + }>; + }; +}; + +export type GetAdminUsersResponse = GetAdminUsersResponses[keyof GetAdminUsersResponses]; + +export type DeleteUserAccountData = { + body?: never; + path: { + userId: string; + accountId: string; + }; + query?: never; + url: '/api/v1/auth/admin-users/{userId}/accounts/{accountId}'; +}; + +export type DeleteUserAccountErrors = { + /** + * Forbidden + */ + 403: unknown; + /** + * Cannot delete the last account + */ + 409: unknown; +}; + +export type DeleteUserAccountResponses = { + /** + * Account deleted successfully + */ + 200: unknown; +}; + export type GetUserDeletionImpactData = { - body?: never; - path: { - userId: string; - }; - query?: never; - url: "/api/v1/auth/deletion-impact/{userId}"; + body?: never; + path: { + userId: string; + }; + query?: never; + url: '/api/v1/auth/deletion-impact/{userId}'; }; export type GetUserDeletionImpactResponses = { - /** - * List of organizations and resources to be deleted - */ - 200: { - organizations: Array<{ - id: string; - name: string; - resources: { - backupSchedulesCount: number; - repositoriesCount: number; - volumesCount: number; - }; - }>; - }; + /** + * List of organizations and resources to be deleted + */ + 200: { + organizations: Array<{ + id: string; + name: string; + resources: { + backupSchedulesCount: number; + repositoriesCount: number; + volumesCount: number; + }; + }>; + }; }; export type GetUserDeletionImpactResponse = GetUserDeletionImpactResponses[keyof GetUserDeletionImpactResponses]; export type ListVolumesData = { - body?: never; - path?: never; - query?: never; - url: "/api/v1/volumes"; + body?: never; + path?: never; + query?: never; + url: '/api/v1/volumes'; }; export type ListVolumesResponses = { - /** - * A list of volumes - */ - 200: Array<{ - autoRemount: boolean; - config: - | { - backend: "directory"; - path: string; - readOnly?: false; - } - | { - backend: "nfs"; - exportPath: string; - server: string; - version: "3" | "4" | "4.1"; - port?: number; - readOnly?: boolean; - } - | { - backend: "rclone"; - path: string; - remote: string; - readOnly?: boolean; - } - | { - backend: "sftp"; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; - readOnly?: boolean; - } - | { - backend: "smb"; - server: string; - share: string; - vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto"; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; - username?: string; - } - | { - backend: "webdav"; - path: string; - server: string; - port?: number; - password?: string; - readOnly?: boolean; - ssl?: boolean; - username?: string; - }; - createdAt: number; - id: number; - lastError: string | null; - lastHealthCheck: number; - name: string; - shortId: string; - status: "error" | "mounted" | "unmounted"; - type: "directory" | "nfs" | "rclone" | "sftp" | "smb" | "webdav"; - updatedAt: number; - }>; + /** + * A list of volumes + */ + 200: Array<{ + autoRemount: boolean; + config: { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'nfs'; + exportPath: string; + server: string; + version: '3' | '4' | '4.1'; + port?: number; + readOnly?: boolean; + } | { + backend: 'rclone'; + path: string; + remote: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + path: string; + username: string; + port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; + password?: string; + privateKey?: string; + readOnly?: boolean; + } | { + backend: 'smb'; + server: string; + share: string; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + port?: number; + domain?: string; + guest?: boolean; + password?: string; + readOnly?: boolean; + username?: string; + } | { + backend: 'webdav'; + path: string; + server: string; + port?: number; + password?: string; + readOnly?: boolean; + ssl?: boolean; + username?: string; + }; + createdAt: number; + id: number; + lastError: string | null; + lastHealthCheck: number; + name: string; + shortId: string; + status: 'error' | 'mounted' | 'unmounted'; + type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; + updatedAt: number; + }>; }; export type ListVolumesResponse = ListVolumesResponses[keyof ListVolumesResponses]; export type CreateVolumeData = { - body?: { - config: - | { - backend: "directory"; - path: string; - readOnly?: false; - } - | { - backend: "nfs"; - exportPath: string; - server: string; - version: "3" | "4" | "4.1"; - port?: number; - readOnly?: boolean; - } - | { - backend: "rclone"; - path: string; - remote: string; - readOnly?: boolean; - } - | { - backend: "sftp"; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; - readOnly?: boolean; - } - | { - backend: "smb"; - server: string; - share: string; - vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto"; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; - username?: string; - } - | { - backend: "webdav"; - path: string; - server: string; - port?: number; - password?: string; - readOnly?: boolean; - ssl?: boolean; - username?: string; - }; - name: string; - }; - path?: never; - query?: never; - url: "/api/v1/volumes"; + body?: { + config: { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'nfs'; + exportPath: string; + server: string; + version: '3' | '4' | '4.1'; + port?: number; + readOnly?: boolean; + } | { + backend: 'rclone'; + path: string; + remote: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + path: string; + username: string; + port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; + password?: string; + privateKey?: string; + readOnly?: boolean; + } | { + backend: 'smb'; + server: string; + share: string; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + port?: number; + domain?: string; + guest?: boolean; + password?: string; + readOnly?: boolean; + username?: string; + } | { + backend: 'webdav'; + path: string; + server: string; + port?: number; + password?: string; + readOnly?: boolean; + ssl?: boolean; + username?: string; + }; + name: string; + }; + path?: never; + query?: never; + url: '/api/v1/volumes'; }; export type CreateVolumeResponses = { - /** - * Volume created successfully - */ - 201: { - autoRemount: boolean; - config: - | { - backend: "directory"; - path: string; - readOnly?: false; - } - | { - backend: "nfs"; - exportPath: string; - server: string; - version: "3" | "4" | "4.1"; - port?: number; - readOnly?: boolean; - } - | { - backend: "rclone"; - path: string; - remote: string; - readOnly?: boolean; - } - | { - backend: "sftp"; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; - readOnly?: boolean; - } - | { - backend: "smb"; - server: string; - share: string; - vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto"; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; - username?: string; - } - | { - backend: "webdav"; - path: string; - server: string; - port?: number; - password?: string; - readOnly?: boolean; - ssl?: boolean; - username?: string; - }; - createdAt: number; - id: number; - lastError: string | null; - lastHealthCheck: number; - name: string; - shortId: string; - status: "error" | "mounted" | "unmounted"; - type: "directory" | "nfs" | "rclone" | "sftp" | "smb" | "webdav"; - updatedAt: number; - }; + /** + * Volume created successfully + */ + 201: { + autoRemount: boolean; + config: { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'nfs'; + exportPath: string; + server: string; + version: '3' | '4' | '4.1'; + port?: number; + readOnly?: boolean; + } | { + backend: 'rclone'; + path: string; + remote: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + path: string; + username: string; + port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; + password?: string; + privateKey?: string; + readOnly?: boolean; + } | { + backend: 'smb'; + server: string; + share: string; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + port?: number; + domain?: string; + guest?: boolean; + password?: string; + readOnly?: boolean; + username?: string; + } | { + backend: 'webdav'; + path: string; + server: string; + port?: number; + password?: string; + readOnly?: boolean; + ssl?: boolean; + username?: string; + }; + createdAt: number; + id: number; + lastError: string | null; + lastHealthCheck: number; + name: string; + shortId: string; + status: 'error' | 'mounted' | 'unmounted'; + type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; + updatedAt: number; + }; }; export type CreateVolumeResponse = CreateVolumeResponses[keyof CreateVolumeResponses]; export type TestConnectionData = { - body?: { - config: - | { - backend: "directory"; - path: string; - readOnly?: false; - } - | { - backend: "nfs"; - exportPath: string; - server: string; - version: "3" | "4" | "4.1"; - port?: number; - readOnly?: boolean; - } - | { - backend: "rclone"; - path: string; - remote: string; - readOnly?: boolean; - } - | { - backend: "sftp"; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; - readOnly?: boolean; - } - | { - backend: "smb"; - server: string; - share: string; - vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto"; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; - username?: string; - } - | { - backend: "webdav"; - path: string; - server: string; - port?: number; - password?: string; - readOnly?: boolean; - ssl?: boolean; - username?: string; - }; - }; - path?: never; - query?: never; - url: "/api/v1/volumes/test-connection"; + body?: { + config: { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'nfs'; + exportPath: string; + server: string; + version: '3' | '4' | '4.1'; + port?: number; + readOnly?: boolean; + } | { + backend: 'rclone'; + path: string; + remote: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + path: string; + username: string; + port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; + password?: string; + privateKey?: string; + readOnly?: boolean; + } | { + backend: 'smb'; + server: string; + share: string; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + port?: number; + domain?: string; + guest?: boolean; + password?: string; + readOnly?: boolean; + username?: string; + } | { + backend: 'webdav'; + path: string; + server: string; + port?: number; + password?: string; + readOnly?: boolean; + ssl?: boolean; + username?: string; + }; + }; + path?: never; + query?: never; + url: '/api/v1/volumes/test-connection'; }; export type TestConnectionResponses = { - /** - * Connection test result - */ - 200: { - message: string; - success: boolean; - }; + /** + * Connection test result + */ + 200: { + message: string; + success: boolean; + }; }; export type TestConnectionResponse = TestConnectionResponses[keyof TestConnectionResponses]; export type DeleteVolumeData = { - body?: never; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/volumes/{shortId}"; + body?: never; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/volumes/{shortId}'; }; export type DeleteVolumeResponses = { - /** - * Volume deleted successfully - */ - 200: { - message: string; - }; + /** + * Volume deleted successfully + */ + 200: { + message: string; + }; }; export type DeleteVolumeResponse = DeleteVolumeResponses[keyof DeleteVolumeResponses]; export type GetVolumeData = { - body?: never; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/volumes/{shortId}"; + body?: never; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/volumes/{shortId}'; }; export type GetVolumeErrors = { - /** - * Volume not found - */ - 404: unknown; + /** + * Volume not found + */ + 404: unknown; }; export type GetVolumeResponses = { - /** - * Volume details - */ - 200: { - statfs: { - free: number; - total: number; - used: number; - }; - volume: { - autoRemount: boolean; - config: - | { - backend: "directory"; - path: string; - readOnly?: false; - } - | { - backend: "nfs"; - exportPath: string; - server: string; - version: "3" | "4" | "4.1"; - port?: number; - readOnly?: boolean; - } - | { - backend: "rclone"; - path: string; - remote: string; - readOnly?: boolean; - } - | { - backend: "sftp"; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; - readOnly?: boolean; - } - | { - backend: "smb"; - server: string; - share: string; - vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto"; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; - username?: string; - } - | { - backend: "webdav"; - path: string; - server: string; - port?: number; - password?: string; - readOnly?: boolean; - ssl?: boolean; - username?: string; - }; - createdAt: number; - id: number; - lastError: string | null; - lastHealthCheck: number; - name: string; - shortId: string; - status: "error" | "mounted" | "unmounted"; - type: "directory" | "nfs" | "rclone" | "sftp" | "smb" | "webdav"; - updatedAt: number; - }; - }; + /** + * Volume details + */ + 200: { + statfs: { + free: number; + total: number; + used: number; + }; + volume: { + autoRemount: boolean; + config: { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'nfs'; + exportPath: string; + server: string; + version: '3' | '4' | '4.1'; + port?: number; + readOnly?: boolean; + } | { + backend: 'rclone'; + path: string; + remote: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + path: string; + username: string; + port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; + password?: string; + privateKey?: string; + readOnly?: boolean; + } | { + backend: 'smb'; + server: string; + share: string; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + port?: number; + domain?: string; + guest?: boolean; + password?: string; + readOnly?: boolean; + username?: string; + } | { + backend: 'webdav'; + path: string; + server: string; + port?: number; + password?: string; + readOnly?: boolean; + ssl?: boolean; + username?: string; + }; + createdAt: number; + id: number; + lastError: string | null; + lastHealthCheck: number; + name: string; + shortId: string; + status: 'error' | 'mounted' | 'unmounted'; + type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; + updatedAt: number; + }; + }; }; export type GetVolumeResponse = GetVolumeResponses[keyof GetVolumeResponses]; export type UpdateVolumeData = { - body?: { - autoRemount?: boolean; - config?: - | { - backend: "directory"; - path: string; - readOnly?: false; - } - | { - backend: "nfs"; - exportPath: string; - server: string; - version: "3" | "4" | "4.1"; - port?: number; - readOnly?: boolean; - } - | { - backend: "rclone"; - path: string; - remote: string; - readOnly?: boolean; - } - | { - backend: "sftp"; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; - readOnly?: boolean; - } - | { - backend: "smb"; - server: string; - share: string; - vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto"; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; - username?: string; - } - | { - backend: "webdav"; - path: string; - server: string; - port?: number; - password?: string; - readOnly?: boolean; - ssl?: boolean; - username?: string; - }; - name?: string; - }; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/volumes/{shortId}"; + body?: { + autoRemount?: boolean; + config?: { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'nfs'; + exportPath: string; + server: string; + version: '3' | '4' | '4.1'; + port?: number; + readOnly?: boolean; + } | { + backend: 'rclone'; + path: string; + remote: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + path: string; + username: string; + port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; + password?: string; + privateKey?: string; + readOnly?: boolean; + } | { + backend: 'smb'; + server: string; + share: string; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + port?: number; + domain?: string; + guest?: boolean; + password?: string; + readOnly?: boolean; + username?: string; + } | { + backend: 'webdav'; + path: string; + server: string; + port?: number; + password?: string; + readOnly?: boolean; + ssl?: boolean; + username?: string; + }; + name?: string; + }; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/volumes/{shortId}'; }; export type UpdateVolumeErrors = { - /** - * Volume not found - */ - 404: unknown; + /** + * Volume not found + */ + 404: unknown; }; export type UpdateVolumeResponses = { - /** - * Volume updated successfully - */ - 200: { - autoRemount: boolean; - config: - | { - backend: "directory"; - path: string; - readOnly?: false; - } - | { - backend: "nfs"; - exportPath: string; - server: string; - version: "3" | "4" | "4.1"; - port?: number; - readOnly?: boolean; - } - | { - backend: "rclone"; - path: string; - remote: string; - readOnly?: boolean; - } - | { - backend: "sftp"; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; - readOnly?: boolean; - } - | { - backend: "smb"; - server: string; - share: string; - vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto"; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; - username?: string; - } - | { - backend: "webdav"; - path: string; - server: string; - port?: number; - password?: string; - readOnly?: boolean; - ssl?: boolean; - username?: string; - }; - createdAt: number; - id: number; - lastError: string | null; - lastHealthCheck: number; - name: string; - shortId: string; - status: "error" | "mounted" | "unmounted"; - type: "directory" | "nfs" | "rclone" | "sftp" | "smb" | "webdav"; - updatedAt: number; - }; + /** + * Volume updated successfully + */ + 200: { + autoRemount: boolean; + config: { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'nfs'; + exportPath: string; + server: string; + version: '3' | '4' | '4.1'; + port?: number; + readOnly?: boolean; + } | { + backend: 'rclone'; + path: string; + remote: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + path: string; + username: string; + port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; + password?: string; + privateKey?: string; + readOnly?: boolean; + } | { + backend: 'smb'; + server: string; + share: string; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + port?: number; + domain?: string; + guest?: boolean; + password?: string; + readOnly?: boolean; + username?: string; + } | { + backend: 'webdav'; + path: string; + server: string; + port?: number; + password?: string; + readOnly?: boolean; + ssl?: boolean; + username?: string; + }; + createdAt: number; + id: number; + lastError: string | null; + lastHealthCheck: number; + name: string; + shortId: string; + status: 'error' | 'mounted' | 'unmounted'; + type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; + updatedAt: number; + }; }; export type UpdateVolumeResponse = UpdateVolumeResponses[keyof UpdateVolumeResponses]; export type MountVolumeData = { - body?: never; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/volumes/{shortId}/mount"; + body?: never; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/volumes/{shortId}/mount'; }; export type MountVolumeResponses = { - /** - * Volume mounted successfully - */ - 200: { - status: "error" | "mounted" | "unmounted"; - error?: string; - }; + /** + * Volume mounted successfully + */ + 200: { + status: 'error' | 'mounted' | 'unmounted'; + error?: string; + }; }; export type MountVolumeResponse = MountVolumeResponses[keyof MountVolumeResponses]; export type UnmountVolumeData = { - body?: never; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/volumes/{shortId}/unmount"; + body?: never; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/volumes/{shortId}/unmount'; }; export type UnmountVolumeResponses = { - /** - * Volume unmounted successfully - */ - 200: { - status: "error" | "mounted" | "unmounted"; - error?: string; - }; + /** + * Volume unmounted successfully + */ + 200: { + status: 'error' | 'mounted' | 'unmounted'; + error?: string; + }; }; export type UnmountVolumeResponse = UnmountVolumeResponses[keyof UnmountVolumeResponses]; export type HealthCheckVolumeData = { - body?: never; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/volumes/{shortId}/health-check"; + body?: never; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/volumes/{shortId}/health-check'; }; export type HealthCheckVolumeErrors = { - /** - * Volume not found - */ - 404: unknown; + /** + * Volume not found + */ + 404: unknown; }; export type HealthCheckVolumeResponses = { - /** - * Volume health check result - */ - 200: { - status: "error" | "mounted" | "unmounted"; - error?: string; - }; + /** + * Volume health check result + */ + 200: { + status: 'error' | 'mounted' | 'unmounted'; + error?: string; + }; }; export type HealthCheckVolumeResponse = HealthCheckVolumeResponses[keyof HealthCheckVolumeResponses]; export type ListFilesData = { - body?: never; - path: { - shortId: string; - }; - query?: { - limit?: string; - offset?: string; - path?: string; - }; - url: "/api/v1/volumes/{shortId}/files"; + body?: never; + path: { + shortId: string; + }; + query?: { + limit?: string; + offset?: string; + path?: string; + }; + url: '/api/v1/volumes/{shortId}/files'; }; export type ListFilesResponses = { - /** - * List of files in the volume - */ - 200: { - files: Array<{ - name: string; - path: string; - type: "directory" | "file"; - modifiedAt?: number; - size?: number; - }>; - hasMore: boolean; - limit: number; - offset: number; - path: string; - total: number; - }; + /** + * List of files in the volume + */ + 200: { + files: Array<{ + name: string; + path: string; + type: 'directory' | 'file'; + modifiedAt?: number; + size?: number; + }>; + hasMore: boolean; + limit: number; + offset: number; + path: string; + total: number; + }; }; export type ListFilesResponse = ListFilesResponses[keyof ListFilesResponses]; export type BrowseFilesystemData = { - body?: never; - path?: never; - query?: { - /** - * Directory path to browse (absolute path, defaults to /) - */ - path?: string; - }; - url: "/api/v1/volumes/filesystem/browse"; + body?: never; + path?: never; + query?: { + /** + * Directory path to browse (absolute path, defaults to /) + */ + path?: string; + }; + url: '/api/v1/volumes/filesystem/browse'; }; export type BrowseFilesystemResponses = { - /** - * List of directories in the specified path - */ - 200: { - directories: Array<{ - name: string; - path: string; - type: "directory" | "file"; - modifiedAt?: number; - size?: number; - }>; - path: string; - }; + /** + * List of directories in the specified path + */ + 200: { + directories: Array<{ + name: string; + path: string; + type: 'directory' | 'file'; + modifiedAt?: number; + size?: number; + }>; + path: string; + }; }; export type BrowseFilesystemResponse = BrowseFilesystemResponses[keyof BrowseFilesystemResponses]; export type ListRepositoriesData = { - body?: never; - path?: never; - query?: never; - url: "/api/v1/repositories"; + body?: never; + path?: never; + query?: never; + url: '/api/v1/repositories'; }; export type ListRepositoriesResponses = { - /** - * List of repositories - */ - 200: Array<{ - compressionMode: "auto" | "max" | "off" | null; - config: - | { - accessKeyId: string; - backend: "r2"; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - accessKeyId: string; - backend: "s3"; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - accountKey: string; - accountName: string; - backend: "azure"; - container: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - endpointSuffix?: string; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "gcs"; - bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "local"; - path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "rclone"; - path: string; - remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "rest"; - url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - password?: string; - path?: string; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - username?: string; - } - | { - backend: "sftp"; - host: string; - path: string; - privateKey: string; - user: string; - port?: number; - skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - knownHosts?: string; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - }; - createdAt: number; - doctorResult: { - completedAt: number; - steps: Array<{ - error: string | null; - output: string | null; - step: string; - success: boolean; - }>; - success: boolean; - } | null; - id: string; - lastChecked: number | null; - lastError: string | null; - name: string; - shortId: string; - status: "cancelled" | "doctor" | "error" | "healthy" | "unknown" | null; - type: "azure" | "gcs" | "local" | "r2" | "rclone" | "rest" | "s3" | "sftp"; - updatedAt: number; - }>; + /** + * List of repositories + */ + 200: Array<{ + compressionMode: 'auto' | 'max' | 'off' | null; + config: { + accessKeyId: string; + backend: 'r2'; + bucket: string; + endpoint: string; + secretAccessKey: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + accessKeyId: string; + backend: 's3'; + bucket: string; + endpoint: string; + secretAccessKey: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + accountKey: string; + accountName: string; + backend: 'azure'; + container: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + endpointSuffix?: string; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'gcs'; + bucket: string; + credentialsJson: string; + projectId: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'local'; + path: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'rclone'; + path: string; + remote: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'rest'; + url: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + password?: string; + path?: string; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + username?: string; + } | { + backend: 'sftp'; + host: string; + path: string; + privateKey: string; + user: string; + port?: number; + skipHostKeyCheck?: boolean; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + knownHosts?: string; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + }; + createdAt: number; + doctorResult: { + completedAt: number; + steps: Array<{ + error: string | null; + output: string | null; + step: string; + success: boolean; + }>; + success: boolean; + } | null; + id: string; + lastChecked: number | null; + lastError: string | null; + name: string; + shortId: string; + status: 'cancelled' | 'doctor' | 'error' | 'healthy' | 'unknown' | null; + type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; + updatedAt: number; + }>; }; export type ListRepositoriesResponse = ListRepositoriesResponses[keyof ListRepositoriesResponses]; export type CreateRepositoryData = { - body?: { - config: - | { - accessKeyId: string; - backend: "r2"; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - accessKeyId: string; - backend: "s3"; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - accountKey: string; - accountName: string; - backend: "azure"; - container: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - endpointSuffix?: string; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "gcs"; - bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "local"; - path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "rclone"; - path: string; - remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "rest"; - url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - password?: string; - path?: string; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - username?: string; - } - | { - backend: "sftp"; - host: string; - path: string; - privateKey: string; - user: string; - port?: number; - skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - knownHosts?: string; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - }; - name: string; - compressionMode?: "auto" | "max" | "off"; - }; - path?: never; - query?: never; - url: "/api/v1/repositories"; + body?: { + config: { + accessKeyId: string; + backend: 'r2'; + bucket: string; + endpoint: string; + secretAccessKey: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + accessKeyId: string; + backend: 's3'; + bucket: string; + endpoint: string; + secretAccessKey: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + accountKey: string; + accountName: string; + backend: 'azure'; + container: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + endpointSuffix?: string; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'gcs'; + bucket: string; + credentialsJson: string; + projectId: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'local'; + path: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'rclone'; + path: string; + remote: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'rest'; + url: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + password?: string; + path?: string; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + username?: string; + } | { + backend: 'sftp'; + host: string; + path: string; + privateKey: string; + user: string; + port?: number; + skipHostKeyCheck?: boolean; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + knownHosts?: string; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + }; + name: string; + compressionMode?: 'auto' | 'max' | 'off'; + }; + path?: never; + query?: never; + url: '/api/v1/repositories'; }; export type CreateRepositoryResponses = { - /** - * Repository created successfully - */ - 201: { - message: string; - repository: { - id: string; - name: string; - shortId: string; - }; - }; + /** + * Repository created successfully + */ + 201: { + message: string; + repository: { + id: string; + name: string; + shortId: string; + }; + }; }; export type CreateRepositoryResponse = CreateRepositoryResponses[keyof CreateRepositoryResponses]; export type ListRcloneRemotesData = { - body?: never; - path?: never; - query?: never; - url: "/api/v1/repositories/rclone-remotes"; + body?: never; + path?: never; + query?: never; + url: '/api/v1/repositories/rclone-remotes'; }; export type ListRcloneRemotesResponses = { - /** - * List of rclone remotes - */ - 200: Array<{ - name: string; - type: string; - }>; + /** + * List of rclone remotes + */ + 200: Array<{ + name: string; + type: string; + }>; }; export type ListRcloneRemotesResponse = ListRcloneRemotesResponses[keyof ListRcloneRemotesResponses]; export type DeleteRepositoryData = { - body?: never; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/repositories/{shortId}"; + body?: never; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/repositories/{shortId}'; }; export type DeleteRepositoryResponses = { - /** - * Repository deleted successfully - */ - 200: { - message: string; - }; + /** + * Repository deleted successfully + */ + 200: { + message: string; + }; }; export type DeleteRepositoryResponse = DeleteRepositoryResponses[keyof DeleteRepositoryResponses]; export type GetRepositoryData = { - body?: never; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/repositories/{shortId}"; + body?: never; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/repositories/{shortId}'; }; export type GetRepositoryResponses = { - /** - * Repository details - */ - 200: { - compressionMode: "auto" | "max" | "off" | null; - config: - | { - accessKeyId: string; - backend: "r2"; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - accessKeyId: string; - backend: "s3"; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - accountKey: string; - accountName: string; - backend: "azure"; - container: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - endpointSuffix?: string; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "gcs"; - bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "local"; - path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "rclone"; - path: string; - remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "rest"; - url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - password?: string; - path?: string; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - username?: string; - } - | { - backend: "sftp"; - host: string; - path: string; - privateKey: string; - user: string; - port?: number; - skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - knownHosts?: string; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - }; - createdAt: number; - doctorResult: { - completedAt: number; - steps: Array<{ - error: string | null; - output: string | null; - step: string; - success: boolean; - }>; - success: boolean; - } | null; - id: string; - lastChecked: number | null; - lastError: string | null; - name: string; - shortId: string; - status: "cancelled" | "doctor" | "error" | "healthy" | "unknown" | null; - type: "azure" | "gcs" | "local" | "r2" | "rclone" | "rest" | "s3" | "sftp"; - updatedAt: number; - }; + /** + * Repository details + */ + 200: { + compressionMode: 'auto' | 'max' | 'off' | null; + config: { + accessKeyId: string; + backend: 'r2'; + bucket: string; + endpoint: string; + secretAccessKey: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + accessKeyId: string; + backend: 's3'; + bucket: string; + endpoint: string; + secretAccessKey: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + accountKey: string; + accountName: string; + backend: 'azure'; + container: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + endpointSuffix?: string; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'gcs'; + bucket: string; + credentialsJson: string; + projectId: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'local'; + path: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'rclone'; + path: string; + remote: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'rest'; + url: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + password?: string; + path?: string; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + username?: string; + } | { + backend: 'sftp'; + host: string; + path: string; + privateKey: string; + user: string; + port?: number; + skipHostKeyCheck?: boolean; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + knownHosts?: string; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + }; + createdAt: number; + doctorResult: { + completedAt: number; + steps: Array<{ + error: string | null; + output: string | null; + step: string; + success: boolean; + }>; + success: boolean; + } | null; + id: string; + lastChecked: number | null; + lastError: string | null; + name: string; + shortId: string; + status: 'cancelled' | 'doctor' | 'error' | 'healthy' | 'unknown' | null; + type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; + updatedAt: number; + }; }; export type GetRepositoryResponse = GetRepositoryResponses[keyof GetRepositoryResponses]; export type UpdateRepositoryData = { - body?: { - compressionMode?: "auto" | "max" | "off"; - config?: - | { - accessKeyId: string; - backend: "r2"; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - accessKeyId: string; - backend: "s3"; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - accountKey: string; - accountName: string; - backend: "azure"; - container: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - endpointSuffix?: string; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "gcs"; - bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "local"; - path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "rclone"; - path: string; - remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "rest"; - url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - password?: string; - path?: string; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - username?: string; - } - | { - backend: "sftp"; - host: string; - path: string; - privateKey: string; - user: string; - port?: number; - skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - knownHosts?: string; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - }; - name?: string; - }; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/repositories/{shortId}"; + body?: { + compressionMode?: 'auto' | 'max' | 'off'; + config?: { + accessKeyId: string; + backend: 'r2'; + bucket: string; + endpoint: string; + secretAccessKey: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + accessKeyId: string; + backend: 's3'; + bucket: string; + endpoint: string; + secretAccessKey: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + accountKey: string; + accountName: string; + backend: 'azure'; + container: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + endpointSuffix?: string; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'gcs'; + bucket: string; + credentialsJson: string; + projectId: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'local'; + path: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'rclone'; + path: string; + remote: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'rest'; + url: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + password?: string; + path?: string; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + username?: string; + } | { + backend: 'sftp'; + host: string; + path: string; + privateKey: string; + user: string; + port?: number; + skipHostKeyCheck?: boolean; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + knownHosts?: string; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + }; + name?: string; + }; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/repositories/{shortId}'; }; export type UpdateRepositoryErrors = { - /** - * Invalid repository update payload - */ - 400: unknown; - /** - * Repository not found - */ - 404: unknown; - /** - * Repository with this name already exists - */ - 409: unknown; + /** + * Invalid repository update payload + */ + 400: unknown; + /** + * Repository not found + */ + 404: unknown; + /** + * Repository with this name already exists + */ + 409: unknown; }; export type UpdateRepositoryResponses = { - /** - * Repository updated successfully - */ - 200: { - compressionMode: "auto" | "max" | "off" | null; - config: - | { - accessKeyId: string; - backend: "r2"; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - accessKeyId: string; - backend: "s3"; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - accountKey: string; - accountName: string; - backend: "azure"; - container: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - endpointSuffix?: string; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "gcs"; - bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "local"; - path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "rclone"; - path: string; - remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "rest"; - url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - password?: string; - path?: string; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - username?: string; - } - | { - backend: "sftp"; - host: string; - path: string; - privateKey: string; - user: string; - port?: number; - skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - knownHosts?: string; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - }; - createdAt: number; - doctorResult: { - completedAt: number; - steps: Array<{ - error: string | null; - output: string | null; - step: string; - success: boolean; - }>; - success: boolean; - } | null; - id: string; - lastChecked: number | null; - lastError: string | null; - name: string; - shortId: string; - status: "cancelled" | "doctor" | "error" | "healthy" | "unknown" | null; - type: "azure" | "gcs" | "local" | "r2" | "rclone" | "rest" | "s3" | "sftp"; - updatedAt: number; - }; + /** + * Repository updated successfully + */ + 200: { + compressionMode: 'auto' | 'max' | 'off' | null; + config: { + accessKeyId: string; + backend: 'r2'; + bucket: string; + endpoint: string; + secretAccessKey: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + accessKeyId: string; + backend: 's3'; + bucket: string; + endpoint: string; + secretAccessKey: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + accountKey: string; + accountName: string; + backend: 'azure'; + container: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + endpointSuffix?: string; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'gcs'; + bucket: string; + credentialsJson: string; + projectId: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'local'; + path: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'rclone'; + path: string; + remote: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'rest'; + url: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + password?: string; + path?: string; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + username?: string; + } | { + backend: 'sftp'; + host: string; + path: string; + privateKey: string; + user: string; + port?: number; + skipHostKeyCheck?: boolean; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + knownHosts?: string; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + }; + createdAt: number; + doctorResult: { + completedAt: number; + steps: Array<{ + error: string | null; + output: string | null; + step: string; + success: boolean; + }>; + success: boolean; + } | null; + id: string; + lastChecked: number | null; + lastError: string | null; + name: string; + shortId: string; + status: 'cancelled' | 'doctor' | 'error' | 'healthy' | 'unknown' | null; + type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; + updatedAt: number; + }; }; export type UpdateRepositoryResponse = UpdateRepositoryResponses[keyof UpdateRepositoryResponses]; export type GetRepositoryStatsData = { - body?: never; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/repositories/{shortId}/stats"; + body?: never; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/repositories/{shortId}/stats'; }; export type GetRepositoryStatsResponses = { - /** - * Repository statistics - */ - 200: { - compression_progress?: number; - compression_ratio?: number; - compression_space_saving?: number; - snapshots_count?: number; - total_size?: number; - total_uncompressed_size?: number; - }; + /** + * Repository statistics + */ + 200: { + compression_progress?: number; + compression_ratio?: number; + compression_space_saving?: number; + snapshots_count?: number; + total_size?: number; + total_uncompressed_size?: number; + }; }; export type GetRepositoryStatsResponse = GetRepositoryStatsResponses[keyof GetRepositoryStatsResponses]; export type DeleteSnapshotsData = { - body?: { - snapshotIds: Array; - }; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/repositories/{shortId}/snapshots"; + body?: { + snapshotIds: Array; + }; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/repositories/{shortId}/snapshots'; }; export type DeleteSnapshotsResponses = { - /** - * Snapshots deleted successfully - */ - 200: { - message: string; - }; + /** + * Snapshots deleted successfully + */ + 200: { + message: string; + }; }; export type DeleteSnapshotsResponse = DeleteSnapshotsResponses[keyof DeleteSnapshotsResponses]; export type ListSnapshotsData = { - body?: never; - path: { - shortId: string; - }; - query?: { - backupId?: string; - }; - url: "/api/v1/repositories/{shortId}/snapshots"; + body?: never; + path: { + shortId: string; + }; + query?: { + backupId?: string; + }; + url: '/api/v1/repositories/{shortId}/snapshots'; }; export type ListSnapshotsResponses = { - /** - * List of snapshots - */ - 200: Array<{ - duration: number; - paths: Array; - retentionCategories: Array; - short_id: string; - size: number; - tags: Array; - time: number; - hostname?: string; - summary?: { - backup_end: string; - backup_start: string; - data_added: number; - data_blobs: number; - dirs_changed: number; - dirs_new: number; - dirs_unmodified: number; - files_changed: number; - files_new: number; - files_unmodified: number; - total_bytes_processed: number; - total_files_processed: number; - tree_blobs: number; - data_added_packed?: number; - }; - }>; + /** + * List of snapshots + */ + 200: Array<{ + duration: number; + paths: Array; + retentionCategories: Array; + short_id: string; + size: number; + tags: Array; + time: number; + hostname?: string; + summary?: { + backup_end: string; + backup_start: string; + data_added: number; + data_blobs: number; + dirs_changed: number; + dirs_new: number; + dirs_unmodified: number; + files_changed: number; + files_new: number; + files_unmodified: number; + total_bytes_processed: number; + total_files_processed: number; + tree_blobs: number; + data_added_packed?: number; + }; + }>; }; export type ListSnapshotsResponse = ListSnapshotsResponses[keyof ListSnapshotsResponses]; export type RefreshSnapshotsData = { - body?: never; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/repositories/{shortId}/snapshots/refresh"; + body?: never; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/repositories/{shortId}/snapshots/refresh'; }; export type RefreshSnapshotsResponses = { - /** - * Snapshot cache cleared and refreshed - */ - 200: { - count: number; - message: string; - }; + /** + * Snapshot cache cleared and refreshed + */ + 200: { + count: number; + message: string; + }; }; export type RefreshSnapshotsResponse = RefreshSnapshotsResponses[keyof RefreshSnapshotsResponses]; export type DeleteSnapshotData = { - body?: never; - path: { - shortId: string; - snapshotId: string; - }; - query?: never; - url: "/api/v1/repositories/{shortId}/snapshots/{snapshotId}"; + body?: never; + path: { + shortId: string; + snapshotId: string; + }; + query?: never; + url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}'; }; export type DeleteSnapshotResponses = { - /** - * Snapshot deleted successfully - */ - 200: { - message: string; - }; + /** + * Snapshot deleted successfully + */ + 200: { + message: string; + }; }; export type DeleteSnapshotResponse = DeleteSnapshotResponses[keyof DeleteSnapshotResponses]; export type GetSnapshotDetailsData = { - body?: never; - path: { - shortId: string; - snapshotId: string; - }; - query?: never; - url: "/api/v1/repositories/{shortId}/snapshots/{snapshotId}"; + body?: never; + path: { + shortId: string; + snapshotId: string; + }; + query?: never; + url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}'; }; export type GetSnapshotDetailsResponses = { - /** - * Snapshot details - */ - 200: { - duration: number; - paths: Array; - retentionCategories: Array; - short_id: string; - size: number; - tags: Array; - time: number; - hostname?: string; - summary?: { - backup_end: string; - backup_start: string; - data_added: number; - data_blobs: number; - dirs_changed: number; - dirs_new: number; - dirs_unmodified: number; - files_changed: number; - files_new: number; - files_unmodified: number; - total_bytes_processed: number; - total_files_processed: number; - tree_blobs: number; - data_added_packed?: number; - }; - }; + /** + * Snapshot details + */ + 200: { + duration: number; + paths: Array; + retentionCategories: Array; + short_id: string; + size: number; + tags: Array; + time: number; + hostname?: string; + summary?: { + backup_end: string; + backup_start: string; + data_added: number; + data_blobs: number; + dirs_changed: number; + dirs_new: number; + dirs_unmodified: number; + files_changed: number; + files_new: number; + files_unmodified: number; + total_bytes_processed: number; + total_files_processed: number; + tree_blobs: number; + data_added_packed?: number; + }; + }; }; export type GetSnapshotDetailsResponse = GetSnapshotDetailsResponses[keyof GetSnapshotDetailsResponses]; export type ListSnapshotFilesData = { - body?: never; - path: { - shortId: string; - snapshotId: string; - }; - query?: { - limit?: string; - offset?: string; - path?: string; - }; - url: "/api/v1/repositories/{shortId}/snapshots/{snapshotId}/files"; + body?: never; + path: { + shortId: string; + snapshotId: string; + }; + query?: { + limit?: string; + offset?: string; + path?: string; + }; + url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}/files'; }; export type ListSnapshotFilesResponses = { - /** - * List of files and directories in the snapshot - */ - 200: { - files: Array<{ - name: string; - path: string; - type: string; - atime?: string; - ctime?: string; - gid?: number; - mode?: number; - mtime?: string; - size?: number; - uid?: number; - }>; - hasMore: boolean; - limit: number; - offset: number; - snapshot: { - hostname: string; - id: string; - paths: Array; - short_id: string; - time: string; - }; - total: number; - }; + /** + * List of files and directories in the snapshot + */ + 200: { + files: Array<{ + name: string; + path: string; + type: string; + atime?: string; + ctime?: string; + gid?: number; + mode?: number; + mtime?: string; + size?: number; + uid?: number; + }>; + hasMore: boolean; + limit: number; + offset: number; + snapshot: { + hostname: string; + id: string; + paths: Array; + short_id: string; + time: string; + }; + total: number; + }; }; export type ListSnapshotFilesResponse = ListSnapshotFilesResponses[keyof ListSnapshotFilesResponses]; export type DumpSnapshotData = { - body?: never; - path: { - shortId: string; - snapshotId: string; - }; - query?: { - kind?: "dir" | "file"; - path?: string; - }; - url: "/api/v1/repositories/{shortId}/snapshots/{snapshotId}/dump"; + body?: never; + path: { + shortId: string; + snapshotId: string; + }; + query?: { + kind?: 'dir' | 'file'; + path?: string; + }; + url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}/dump'; }; export type DumpSnapshotResponses = { - /** - * Snapshot content stream - */ - 200: Blob | File; + /** + * Snapshot content stream + */ + 200: Blob | File; }; export type DumpSnapshotResponse = DumpSnapshotResponses[keyof DumpSnapshotResponses]; export type RestoreSnapshotData = { - body?: { - snapshotId: string; - delete?: boolean; - exclude?: Array; - excludeXattr?: Array; - include?: Array; - overwrite?: "always" | "if-changed" | "if-newer" | "never"; - targetPath?: string; - }; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/repositories/{shortId}/restore"; + body?: { + snapshotId: string; + delete?: boolean; + exclude?: Array; + excludeXattr?: Array; + include?: Array; + overwrite?: 'always' | 'if-changed' | 'if-newer' | 'never'; + targetPath?: string; + }; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/repositories/{shortId}/restore'; }; export type RestoreSnapshotResponses = { - /** - * Snapshot restored successfully - */ - 200: { - filesRestored: number; - filesSkipped: number; - message: string; - success: boolean; - }; + /** + * Snapshot restored successfully + */ + 200: { + filesRestored: number; + filesSkipped: number; + message: string; + success: boolean; + }; }; export type RestoreSnapshotResponse = RestoreSnapshotResponses[keyof RestoreSnapshotResponses]; export type CancelDoctorData = { - body?: never; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/repositories/{shortId}/doctor"; + body?: never; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/repositories/{shortId}/doctor'; }; export type CancelDoctorErrors = { - /** - * No doctor operation is currently running - */ - 409: unknown; + /** + * No doctor operation is currently running + */ + 409: unknown; }; export type CancelDoctorResponses = { - /** - * Doctor operation cancelled - */ - 200: { - message: string; - }; + /** + * Doctor operation cancelled + */ + 200: { + message: string; + }; }; export type CancelDoctorResponse = CancelDoctorResponses[keyof CancelDoctorResponses]; export type StartDoctorData = { - body?: never; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/repositories/{shortId}/doctor"; + body?: never; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/repositories/{shortId}/doctor'; }; export type StartDoctorErrors = { - /** - * Doctor operation already in progress - */ - 409: unknown; + /** + * Doctor operation already in progress + */ + 409: unknown; }; export type StartDoctorResponses = { - /** - * Doctor operation started - */ - 202: { - message: string; - repositoryId: string; - }; + /** + * Doctor operation started + */ + 202: { + message: string; + repositoryId: string; + }; }; export type StartDoctorResponse = StartDoctorResponses[keyof StartDoctorResponses]; export type UnlockRepositoryData = { - body?: never; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/repositories/{shortId}/unlock"; + body?: never; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/repositories/{shortId}/unlock'; }; export type UnlockRepositoryResponses = { - /** - * Repository unlocked successfully - */ - 200: { - message: string; - success: boolean; - }; + /** + * Repository unlocked successfully + */ + 200: { + message: string; + success: boolean; + }; }; export type UnlockRepositoryResponse = UnlockRepositoryResponses[keyof UnlockRepositoryResponses]; export type TagSnapshotsData = { - body?: { - snapshotIds: Array; - add?: Array; - remove?: Array; - set?: Array; - }; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/repositories/{shortId}/snapshots/tag"; + body?: { + snapshotIds: Array; + add?: Array; + remove?: Array; + set?: Array; + }; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/repositories/{shortId}/snapshots/tag'; }; export type TagSnapshotsResponses = { - /** - * Snapshots tagged successfully - */ - 200: { - message: string; - }; + /** + * Snapshots tagged successfully + */ + 200: { + message: string; + }; }; export type TagSnapshotsResponse = TagSnapshotsResponses[keyof TagSnapshotsResponses]; export type DevPanelExecData = { - body?: { - command: string; - args?: Array; - }; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/repositories/{shortId}/exec"; + body?: { + command: string; + args?: Array; + }; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/repositories/{shortId}/exec'; }; export type DevPanelExecErrors = { - /** - * Dev panel not enabled - */ - 403: unknown; + /** + * Dev panel not enabled + */ + 403: unknown; }; export type DevPanelExecResponses = { - /** - * Command output stream (SSE) - */ - 200: string; + /** + * Command output stream (SSE) + */ + 200: string; }; export type DevPanelExecResponse = DevPanelExecResponses[keyof DevPanelExecResponses]; export type ListBackupSchedulesData = { - body?: never; - path?: never; - query?: never; - url: "/api/v1/backups"; + body?: never; + path?: never; + query?: never; + url: '/api/v1/backups'; }; export type ListBackupSchedulesResponses = { - /** - * List of backup schedules - */ - 200: Array<{ - createdAt: number; - cronExpression: string; - enabled: boolean; - excludeIfPresent: Array | null; - excludePatterns: Array | null; - id: number; - includePatterns: Array | null; - lastBackupAt: number | null; - lastBackupError: string | null; - lastBackupStatus: "error" | "in_progress" | "success" | "warning" | null; - name: string; - nextBackupAt: number | null; - oneFileSystem: boolean; - repository: { - compressionMode: "auto" | "max" | "off" | null; - config: - | { - accessKeyId: string; - backend: "r2"; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - accessKeyId: string; - backend: "s3"; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - accountKey: string; - accountName: string; - backend: "azure"; - container: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - endpointSuffix?: string; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "gcs"; - bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "local"; - path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "rclone"; - path: string; - remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "rest"; - url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - password?: string; - path?: string; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - username?: string; - } - | { - backend: "sftp"; - host: string; - path: string; - privateKey: string; - user: string; - port?: number; - skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - knownHosts?: string; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - }; - createdAt: number; - doctorResult: { - completedAt: number; - steps: Array<{ - error: string | null; - output: string | null; - step: string; - success: boolean; - }>; - success: boolean; - } | null; - id: string; - lastChecked: number | null; - lastError: string | null; - name: string; - shortId: string; - status: "cancelled" | "doctor" | "error" | "healthy" | "unknown" | null; - type: "azure" | "gcs" | "local" | "r2" | "rclone" | "rest" | "s3" | "sftp"; - updatedAt: number; - }; - repositoryId: string; - retentionPolicy: { - keepDaily?: number; - keepHourly?: number; - keepLast?: number; - keepMonthly?: number; - keepWeekly?: number; - keepWithinDuration?: string; - keepYearly?: number; - } | null; - shortId: string; - updatedAt: number; - volume: { - autoRemount: boolean; - config: - | { - backend: "directory"; - path: string; - readOnly?: false; - } - | { - backend: "nfs"; - exportPath: string; - server: string; - version: "3" | "4" | "4.1"; - port?: number; - readOnly?: boolean; - } - | { - backend: "rclone"; - path: string; - remote: string; - readOnly?: boolean; - } - | { - backend: "sftp"; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; - readOnly?: boolean; - } - | { - backend: "smb"; - server: string; - share: string; - vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto"; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; - username?: string; - } - | { - backend: "webdav"; - path: string; - server: string; - port?: number; - password?: string; - readOnly?: boolean; - ssl?: boolean; - username?: string; - }; - createdAt: number; - id: number; - lastError: string | null; - lastHealthCheck: number; - name: string; - shortId: string; - status: "error" | "mounted" | "unmounted"; - type: "directory" | "nfs" | "rclone" | "sftp" | "smb" | "webdav"; - updatedAt: number; - }; - volumeId: number; - }>; + /** + * List of backup schedules + */ + 200: Array<{ + createdAt: number; + cronExpression: string; + enabled: boolean; + excludeIfPresent: Array | null; + excludePatterns: Array | null; + id: number; + includePatterns: Array | null; + lastBackupAt: number | null; + lastBackupError: string | null; + lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; + name: string; + nextBackupAt: number | null; + oneFileSystem: boolean; + repository: { + compressionMode: 'auto' | 'max' | 'off' | null; + config: { + accessKeyId: string; + backend: 'r2'; + bucket: string; + endpoint: string; + secretAccessKey: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + accessKeyId: string; + backend: 's3'; + bucket: string; + endpoint: string; + secretAccessKey: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + accountKey: string; + accountName: string; + backend: 'azure'; + container: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + endpointSuffix?: string; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'gcs'; + bucket: string; + credentialsJson: string; + projectId: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'local'; + path: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'rclone'; + path: string; + remote: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'rest'; + url: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + password?: string; + path?: string; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + username?: string; + } | { + backend: 'sftp'; + host: string; + path: string; + privateKey: string; + user: string; + port?: number; + skipHostKeyCheck?: boolean; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + knownHosts?: string; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + }; + createdAt: number; + doctorResult: { + completedAt: number; + steps: Array<{ + error: string | null; + output: string | null; + step: string; + success: boolean; + }>; + success: boolean; + } | null; + id: string; + lastChecked: number | null; + lastError: string | null; + name: string; + shortId: string; + status: 'cancelled' | 'doctor' | 'error' | 'healthy' | 'unknown' | null; + type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; + updatedAt: number; + }; + repositoryId: string; + retentionPolicy: { + keepDaily?: number; + keepHourly?: number; + keepLast?: number; + keepMonthly?: number; + keepWeekly?: number; + keepWithinDuration?: string; + keepYearly?: number; + } | null; + shortId: string; + updatedAt: number; + volume: { + autoRemount: boolean; + config: { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'nfs'; + exportPath: string; + server: string; + version: '3' | '4' | '4.1'; + port?: number; + readOnly?: boolean; + } | { + backend: 'rclone'; + path: string; + remote: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + path: string; + username: string; + port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; + password?: string; + privateKey?: string; + readOnly?: boolean; + } | { + backend: 'smb'; + server: string; + share: string; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + port?: number; + domain?: string; + guest?: boolean; + password?: string; + readOnly?: boolean; + username?: string; + } | { + backend: 'webdav'; + path: string; + server: string; + port?: number; + password?: string; + readOnly?: boolean; + ssl?: boolean; + username?: string; + }; + createdAt: number; + id: number; + lastError: string | null; + lastHealthCheck: number; + name: string; + shortId: string; + status: 'error' | 'mounted' | 'unmounted'; + type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; + updatedAt: number; + }; + volumeId: number; + }>; }; export type ListBackupSchedulesResponse = ListBackupSchedulesResponses[keyof ListBackupSchedulesResponses]; export type CreateBackupScheduleData = { - body?: { - cronExpression: string; - enabled: boolean; - name: string; - repositoryId: string; - volumeId: number | string; - excludeIfPresent?: Array; - excludePatterns?: Array; - includePatterns?: Array; - oneFileSystem?: boolean; - retentionPolicy?: { - keepDaily?: number; - keepHourly?: number; - keepLast?: number; - keepMonthly?: number; - keepWeekly?: number; - keepWithinDuration?: string; - keepYearly?: number; - }; - tags?: Array; - }; - path?: never; - query?: never; - url: "/api/v1/backups"; + body?: { + cronExpression: string; + enabled: boolean; + name: string; + repositoryId: string; + volumeId: number | string; + excludeIfPresent?: Array; + excludePatterns?: Array; + includePatterns?: Array; + oneFileSystem?: boolean; + retentionPolicy?: { + keepDaily?: number; + keepHourly?: number; + keepLast?: number; + keepMonthly?: number; + keepWeekly?: number; + keepWithinDuration?: string; + keepYearly?: number; + }; + tags?: Array; + }; + path?: never; + query?: never; + url: '/api/v1/backups'; }; export type CreateBackupScheduleResponses = { - /** - * Backup schedule created successfully - */ - 201: { - createdAt: number; - cronExpression: string; - enabled: boolean; - excludeIfPresent: Array | null; - excludePatterns: Array | null; - id: number; - includePatterns: Array | null; - lastBackupAt: number | null; - lastBackupError: string | null; - lastBackupStatus: "error" | "in_progress" | "success" | "warning" | null; - name: string; - nextBackupAt: number | null; - oneFileSystem: boolean; - repositoryId: string; - retentionPolicy: { - keepDaily?: number; - keepHourly?: number; - keepLast?: number; - keepMonthly?: number; - keepWeekly?: number; - keepWithinDuration?: string; - keepYearly?: number; - } | null; - shortId: string; - updatedAt: number; - volumeId: number; - }; + /** + * Backup schedule created successfully + */ + 201: { + createdAt: number; + cronExpression: string; + enabled: boolean; + excludeIfPresent: Array | null; + excludePatterns: Array | null; + id: number; + includePatterns: Array | null; + lastBackupAt: number | null; + lastBackupError: string | null; + lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; + name: string; + nextBackupAt: number | null; + oneFileSystem: boolean; + repositoryId: string; + retentionPolicy: { + keepDaily?: number; + keepHourly?: number; + keepLast?: number; + keepMonthly?: number; + keepWeekly?: number; + keepWithinDuration?: string; + keepYearly?: number; + } | null; + shortId: string; + updatedAt: number; + volumeId: number; + }; }; export type CreateBackupScheduleResponse = CreateBackupScheduleResponses[keyof CreateBackupScheduleResponses]; export type DeleteBackupScheduleData = { - body?: never; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/backups/{shortId}"; + body?: never; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/backups/{shortId}'; }; export type DeleteBackupScheduleResponses = { - /** - * Backup schedule deleted successfully - */ - 200: { - success: boolean; - }; + /** + * Backup schedule deleted successfully + */ + 200: { + success: boolean; + }; }; export type DeleteBackupScheduleResponse = DeleteBackupScheduleResponses[keyof DeleteBackupScheduleResponses]; export type GetBackupScheduleData = { - body?: never; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/backups/{shortId}"; + body?: never; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/backups/{shortId}'; }; export type GetBackupScheduleResponses = { - /** - * Backup schedule details - */ - 200: { - createdAt: number; - cronExpression: string; - enabled: boolean; - excludeIfPresent: Array | null; - excludePatterns: Array | null; - id: number; - includePatterns: Array | null; - lastBackupAt: number | null; - lastBackupError: string | null; - lastBackupStatus: "error" | "in_progress" | "success" | "warning" | null; - name: string; - nextBackupAt: number | null; - oneFileSystem: boolean; - repository: { - compressionMode: "auto" | "max" | "off" | null; - config: - | { - accessKeyId: string; - backend: "r2"; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - accessKeyId: string; - backend: "s3"; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - accountKey: string; - accountName: string; - backend: "azure"; - container: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - endpointSuffix?: string; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "gcs"; - bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "local"; - path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "rclone"; - path: string; - remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "rest"; - url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - password?: string; - path?: string; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - username?: string; - } - | { - backend: "sftp"; - host: string; - path: string; - privateKey: string; - user: string; - port?: number; - skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - knownHosts?: string; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - }; - createdAt: number; - doctorResult: { - completedAt: number; - steps: Array<{ - error: string | null; - output: string | null; - step: string; - success: boolean; - }>; - success: boolean; - } | null; - id: string; - lastChecked: number | null; - lastError: string | null; - name: string; - shortId: string; - status: "cancelled" | "doctor" | "error" | "healthy" | "unknown" | null; - type: "azure" | "gcs" | "local" | "r2" | "rclone" | "rest" | "s3" | "sftp"; - updatedAt: number; - }; - repositoryId: string; - retentionPolicy: { - keepDaily?: number; - keepHourly?: number; - keepLast?: number; - keepMonthly?: number; - keepWeekly?: number; - keepWithinDuration?: string; - keepYearly?: number; - } | null; - shortId: string; - updatedAt: number; - volume: { - autoRemount: boolean; - config: - | { - backend: "directory"; - path: string; - readOnly?: false; - } - | { - backend: "nfs"; - exportPath: string; - server: string; - version: "3" | "4" | "4.1"; - port?: number; - readOnly?: boolean; - } - | { - backend: "rclone"; - path: string; - remote: string; - readOnly?: boolean; - } - | { - backend: "sftp"; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; - readOnly?: boolean; - } - | { - backend: "smb"; - server: string; - share: string; - vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto"; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; - username?: string; - } - | { - backend: "webdav"; - path: string; - server: string; - port?: number; - password?: string; - readOnly?: boolean; - ssl?: boolean; - username?: string; - }; - createdAt: number; - id: number; - lastError: string | null; - lastHealthCheck: number; - name: string; - shortId: string; - status: "error" | "mounted" | "unmounted"; - type: "directory" | "nfs" | "rclone" | "sftp" | "smb" | "webdav"; - updatedAt: number; - }; - volumeId: number; - }; + /** + * Backup schedule details + */ + 200: { + createdAt: number; + cronExpression: string; + enabled: boolean; + excludeIfPresent: Array | null; + excludePatterns: Array | null; + id: number; + includePatterns: Array | null; + lastBackupAt: number | null; + lastBackupError: string | null; + lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; + name: string; + nextBackupAt: number | null; + oneFileSystem: boolean; + repository: { + compressionMode: 'auto' | 'max' | 'off' | null; + config: { + accessKeyId: string; + backend: 'r2'; + bucket: string; + endpoint: string; + secretAccessKey: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + accessKeyId: string; + backend: 's3'; + bucket: string; + endpoint: string; + secretAccessKey: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + accountKey: string; + accountName: string; + backend: 'azure'; + container: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + endpointSuffix?: string; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'gcs'; + bucket: string; + credentialsJson: string; + projectId: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'local'; + path: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'rclone'; + path: string; + remote: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'rest'; + url: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + password?: string; + path?: string; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + username?: string; + } | { + backend: 'sftp'; + host: string; + path: string; + privateKey: string; + user: string; + port?: number; + skipHostKeyCheck?: boolean; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + knownHosts?: string; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + }; + createdAt: number; + doctorResult: { + completedAt: number; + steps: Array<{ + error: string | null; + output: string | null; + step: string; + success: boolean; + }>; + success: boolean; + } | null; + id: string; + lastChecked: number | null; + lastError: string | null; + name: string; + shortId: string; + status: 'cancelled' | 'doctor' | 'error' | 'healthy' | 'unknown' | null; + type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; + updatedAt: number; + }; + repositoryId: string; + retentionPolicy: { + keepDaily?: number; + keepHourly?: number; + keepLast?: number; + keepMonthly?: number; + keepWeekly?: number; + keepWithinDuration?: string; + keepYearly?: number; + } | null; + shortId: string; + updatedAt: number; + volume: { + autoRemount: boolean; + config: { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'nfs'; + exportPath: string; + server: string; + version: '3' | '4' | '4.1'; + port?: number; + readOnly?: boolean; + } | { + backend: 'rclone'; + path: string; + remote: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + path: string; + username: string; + port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; + password?: string; + privateKey?: string; + readOnly?: boolean; + } | { + backend: 'smb'; + server: string; + share: string; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + port?: number; + domain?: string; + guest?: boolean; + password?: string; + readOnly?: boolean; + username?: string; + } | { + backend: 'webdav'; + path: string; + server: string; + port?: number; + password?: string; + readOnly?: boolean; + ssl?: boolean; + username?: string; + }; + createdAt: number; + id: number; + lastError: string | null; + lastHealthCheck: number; + name: string; + shortId: string; + status: 'error' | 'mounted' | 'unmounted'; + type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; + updatedAt: number; + }; + volumeId: number; + }; }; export type GetBackupScheduleResponse = GetBackupScheduleResponses[keyof GetBackupScheduleResponses]; export type UpdateBackupScheduleData = { - body?: { - cronExpression: string; - repositoryId: string; - enabled?: boolean; - excludeIfPresent?: Array; - excludePatterns?: Array; - includePatterns?: Array; - name?: string; - oneFileSystem?: boolean; - retentionPolicy?: { - keepDaily?: number; - keepHourly?: number; - keepLast?: number; - keepMonthly?: number; - keepWeekly?: number; - keepWithinDuration?: string; - keepYearly?: number; - }; - tags?: Array; - }; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/backups/{shortId}"; + body?: { + cronExpression: string; + repositoryId: string; + enabled?: boolean; + excludeIfPresent?: Array; + excludePatterns?: Array; + includePatterns?: Array; + name?: string; + oneFileSystem?: boolean; + retentionPolicy?: { + keepDaily?: number; + keepHourly?: number; + keepLast?: number; + keepMonthly?: number; + keepWeekly?: number; + keepWithinDuration?: string; + keepYearly?: number; + }; + tags?: Array; + }; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/backups/{shortId}'; }; export type UpdateBackupScheduleResponses = { - /** - * Backup schedule updated successfully - */ - 200: { - createdAt: number; - cronExpression: string; - enabled: boolean; - excludeIfPresent: Array | null; - excludePatterns: Array | null; - id: number; - includePatterns: Array | null; - lastBackupAt: number | null; - lastBackupError: string | null; - lastBackupStatus: "error" | "in_progress" | "success" | "warning" | null; - name: string; - nextBackupAt: number | null; - oneFileSystem: boolean; - repositoryId: string; - retentionPolicy: { - keepDaily?: number; - keepHourly?: number; - keepLast?: number; - keepMonthly?: number; - keepWeekly?: number; - keepWithinDuration?: string; - keepYearly?: number; - } | null; - shortId: string; - updatedAt: number; - volumeId: number; - }; + /** + * Backup schedule updated successfully + */ + 200: { + createdAt: number; + cronExpression: string; + enabled: boolean; + excludeIfPresent: Array | null; + excludePatterns: Array | null; + id: number; + includePatterns: Array | null; + lastBackupAt: number | null; + lastBackupError: string | null; + lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; + name: string; + nextBackupAt: number | null; + oneFileSystem: boolean; + repositoryId: string; + retentionPolicy: { + keepDaily?: number; + keepHourly?: number; + keepLast?: number; + keepMonthly?: number; + keepWeekly?: number; + keepWithinDuration?: string; + keepYearly?: number; + } | null; + shortId: string; + updatedAt: number; + volumeId: number; + }; }; export type UpdateBackupScheduleResponse = UpdateBackupScheduleResponses[keyof UpdateBackupScheduleResponses]; export type GetBackupScheduleForVolumeData = { - body?: never; - path: { - volumeShortId: string; - }; - query?: never; - url: "/api/v1/backups/volume/{volumeShortId}"; + body?: never; + path: { + volumeShortId: string; + }; + query?: never; + url: '/api/v1/backups/volume/{volumeShortId}'; }; export type GetBackupScheduleForVolumeResponses = { - /** - * Backup schedule details for the volume - */ - 200: { - createdAt: number; - cronExpression: string; - enabled: boolean; - excludeIfPresent: Array | null; - excludePatterns: Array | null; - id: number; - includePatterns: Array | null; - lastBackupAt: number | null; - lastBackupError: string | null; - lastBackupStatus: "error" | "in_progress" | "success" | "warning" | null; - name: string; - nextBackupAt: number | null; - oneFileSystem: boolean; - repository: { - compressionMode: "auto" | "max" | "off" | null; - config: - | { - accessKeyId: string; - backend: "r2"; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - accessKeyId: string; - backend: "s3"; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - accountKey: string; - accountName: string; - backend: "azure"; - container: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - endpointSuffix?: string; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "gcs"; - bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "local"; - path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "rclone"; - path: string; - remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "rest"; - url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - password?: string; - path?: string; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - username?: string; - } - | { - backend: "sftp"; - host: string; - path: string; - privateKey: string; - user: string; - port?: number; - skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - knownHosts?: string; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - }; - createdAt: number; - doctorResult: { - completedAt: number; - steps: Array<{ - error: string | null; - output: string | null; - step: string; - success: boolean; - }>; - success: boolean; - } | null; - id: string; - lastChecked: number | null; - lastError: string | null; - name: string; - shortId: string; - status: "cancelled" | "doctor" | "error" | "healthy" | "unknown" | null; - type: "azure" | "gcs" | "local" | "r2" | "rclone" | "rest" | "s3" | "sftp"; - updatedAt: number; - }; - repositoryId: string; - retentionPolicy: { - keepDaily?: number; - keepHourly?: number; - keepLast?: number; - keepMonthly?: number; - keepWeekly?: number; - keepWithinDuration?: string; - keepYearly?: number; - } | null; - shortId: string; - updatedAt: number; - volume: { - autoRemount: boolean; - config: - | { - backend: "directory"; - path: string; - readOnly?: false; - } - | { - backend: "nfs"; - exportPath: string; - server: string; - version: "3" | "4" | "4.1"; - port?: number; - readOnly?: boolean; - } - | { - backend: "rclone"; - path: string; - remote: string; - readOnly?: boolean; - } - | { - backend: "sftp"; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; - readOnly?: boolean; - } - | { - backend: "smb"; - server: string; - share: string; - vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto"; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; - username?: string; - } - | { - backend: "webdav"; - path: string; - server: string; - port?: number; - password?: string; - readOnly?: boolean; - ssl?: boolean; - username?: string; - }; - createdAt: number; - id: number; - lastError: string | null; - lastHealthCheck: number; - name: string; - shortId: string; - status: "error" | "mounted" | "unmounted"; - type: "directory" | "nfs" | "rclone" | "sftp" | "smb" | "webdav"; - updatedAt: number; - }; - volumeId: number; - } | null; + /** + * Backup schedule details for the volume + */ + 200: { + createdAt: number; + cronExpression: string; + enabled: boolean; + excludeIfPresent: Array | null; + excludePatterns: Array | null; + id: number; + includePatterns: Array | null; + lastBackupAt: number | null; + lastBackupError: string | null; + lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; + name: string; + nextBackupAt: number | null; + oneFileSystem: boolean; + repository: { + compressionMode: 'auto' | 'max' | 'off' | null; + config: { + accessKeyId: string; + backend: 'r2'; + bucket: string; + endpoint: string; + secretAccessKey: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + accessKeyId: string; + backend: 's3'; + bucket: string; + endpoint: string; + secretAccessKey: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + accountKey: string; + accountName: string; + backend: 'azure'; + container: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + endpointSuffix?: string; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'gcs'; + bucket: string; + credentialsJson: string; + projectId: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'local'; + path: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'rclone'; + path: string; + remote: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'rest'; + url: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + password?: string; + path?: string; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + username?: string; + } | { + backend: 'sftp'; + host: string; + path: string; + privateKey: string; + user: string; + port?: number; + skipHostKeyCheck?: boolean; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + knownHosts?: string; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + }; + createdAt: number; + doctorResult: { + completedAt: number; + steps: Array<{ + error: string | null; + output: string | null; + step: string; + success: boolean; + }>; + success: boolean; + } | null; + id: string; + lastChecked: number | null; + lastError: string | null; + name: string; + shortId: string; + status: 'cancelled' | 'doctor' | 'error' | 'healthy' | 'unknown' | null; + type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; + updatedAt: number; + }; + repositoryId: string; + retentionPolicy: { + keepDaily?: number; + keepHourly?: number; + keepLast?: number; + keepMonthly?: number; + keepWeekly?: number; + keepWithinDuration?: string; + keepYearly?: number; + } | null; + shortId: string; + updatedAt: number; + volume: { + autoRemount: boolean; + config: { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'nfs'; + exportPath: string; + server: string; + version: '3' | '4' | '4.1'; + port?: number; + readOnly?: boolean; + } | { + backend: 'rclone'; + path: string; + remote: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + path: string; + username: string; + port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; + password?: string; + privateKey?: string; + readOnly?: boolean; + } | { + backend: 'smb'; + server: string; + share: string; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + port?: number; + domain?: string; + guest?: boolean; + password?: string; + readOnly?: boolean; + username?: string; + } | { + backend: 'webdav'; + path: string; + server: string; + port?: number; + password?: string; + readOnly?: boolean; + ssl?: boolean; + username?: string; + }; + createdAt: number; + id: number; + lastError: string | null; + lastHealthCheck: number; + name: string; + shortId: string; + status: 'error' | 'mounted' | 'unmounted'; + type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; + updatedAt: number; + }; + volumeId: number; + } | null; }; -export type GetBackupScheduleForVolumeResponse = - GetBackupScheduleForVolumeResponses[keyof GetBackupScheduleForVolumeResponses]; +export type GetBackupScheduleForVolumeResponse = GetBackupScheduleForVolumeResponses[keyof GetBackupScheduleForVolumeResponses]; export type RunBackupNowData = { - body?: never; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/backups/{shortId}/run"; + body?: never; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/backups/{shortId}/run'; }; export type RunBackupNowResponses = { - /** - * Backup started successfully - */ - 200: { - success: boolean; - }; + /** + * Backup started successfully + */ + 200: { + success: boolean; + }; }; export type RunBackupNowResponse = RunBackupNowResponses[keyof RunBackupNowResponses]; export type StopBackupData = { - body?: never; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/backups/{shortId}/stop"; + body?: never; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/backups/{shortId}/stop'; }; export type StopBackupErrors = { - /** - * No backup is currently running for this schedule - */ - 409: unknown; + /** + * No backup is currently running for this schedule + */ + 409: unknown; }; export type StopBackupResponses = { - /** - * Backup stopped successfully - */ - 200: { - success: boolean; - }; + /** + * Backup stopped successfully + */ + 200: { + success: boolean; + }; }; export type StopBackupResponse = StopBackupResponses[keyof StopBackupResponses]; export type RunForgetData = { - body?: never; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/backups/{shortId}/forget"; + body?: never; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/backups/{shortId}/forget'; }; export type RunForgetResponses = { - /** - * Retention policy applied successfully - */ - 200: { - success: boolean; - }; + /** + * Retention policy applied successfully + */ + 200: { + success: boolean; + }; }; export type RunForgetResponse = RunForgetResponses[keyof RunForgetResponses]; export type GetScheduleNotificationsData = { - body?: never; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/backups/{shortId}/notifications"; + body?: never; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/backups/{shortId}/notifications'; }; export type GetScheduleNotificationsResponses = { - /** - * List of notification assignments for the schedule - */ - 200: Array<{ - createdAt: number; - destination: { - config: - | { - apiToken: string; - priority: -1 | 0 | 1; - type: "pushover"; - userKey: string; - devices?: string; - } - | { - botToken: string; - chatId: string; - type: "telegram"; - threadId?: string; - } - | { - from: string; - smtpHost: string; - smtpPort: number; - to: Array; - type: "email"; - useTLS: boolean; - fromName?: string; - password?: string; - username?: string; - } - | { - method: "GET" | "POST"; - type: "generic"; - url: string; - contentType?: string; - headers?: Array; - messageKey?: string; - titleKey?: string; - useJson?: boolean; - } - | { - priority: "default" | "high" | "low" | "max" | "min"; - topic: string; - type: "ntfy"; - accessToken?: string; - password?: string; - serverUrl?: string; - username?: string; - } - | { - priority: number; - serverUrl: string; - token: string; - type: "gotify"; - path?: string; - } - | { - shoutrrrUrl: string; - type: "custom"; - } - | { - type: "discord"; - webhookUrl: string; - avatarUrl?: string; - threadId?: string; - username?: string; - } - | { - type: "slack"; - webhookUrl: string; - channel?: string; - iconEmoji?: string; - username?: string; - }; - createdAt: number; - enabled: boolean; - id: number; - name: string; - type: "custom" | "discord" | "email" | "generic" | "gotify" | "ntfy" | "pushover" | "slack" | "telegram"; - updatedAt: number; - }; - destinationId: number; - notifyOnFailure: boolean; - notifyOnStart: boolean; - notifyOnSuccess: boolean; - notifyOnWarning: boolean; - scheduleId: number; - }>; + /** + * List of notification assignments for the schedule + */ + 200: Array<{ + createdAt: number; + destination: { + config: { + apiToken: string; + priority: -1 | 0 | 1; + type: 'pushover'; + userKey: string; + devices?: string; + } | { + botToken: string; + chatId: string; + type: 'telegram'; + threadId?: string; + } | { + from: string; + smtpHost: string; + smtpPort: number; + to: Array; + type: 'email'; + useTLS: boolean; + fromName?: string; + password?: string; + username?: string; + } | { + method: 'GET' | 'POST'; + type: 'generic'; + url: string; + contentType?: string; + headers?: Array; + messageKey?: string; + titleKey?: string; + useJson?: boolean; + } | { + priority: 'default' | 'high' | 'low' | 'max' | 'min'; + topic: string; + type: 'ntfy'; + accessToken?: string; + password?: string; + serverUrl?: string; + username?: string; + } | { + priority: number; + serverUrl: string; + token: string; + type: 'gotify'; + path?: string; + } | { + shoutrrrUrl: string; + type: 'custom'; + } | { + type: 'discord'; + webhookUrl: string; + avatarUrl?: string; + threadId?: string; + username?: string; + } | { + type: 'slack'; + webhookUrl: string; + channel?: string; + iconEmoji?: string; + username?: string; + }; + createdAt: number; + enabled: boolean; + id: number; + name: string; + type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; + updatedAt: number; + }; + destinationId: number; + notifyOnFailure: boolean; + notifyOnStart: boolean; + notifyOnSuccess: boolean; + notifyOnWarning: boolean; + scheduleId: number; + }>; }; -export type GetScheduleNotificationsResponse = - GetScheduleNotificationsResponses[keyof GetScheduleNotificationsResponses]; +export type GetScheduleNotificationsResponse = GetScheduleNotificationsResponses[keyof GetScheduleNotificationsResponses]; export type UpdateScheduleNotificationsData = { - body?: { - assignments: Array<{ - destinationId: number; - notifyOnFailure: boolean; - notifyOnStart: boolean; - notifyOnSuccess: boolean; - notifyOnWarning: boolean; - }>; - }; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/backups/{shortId}/notifications"; + body?: { + assignments: Array<{ + destinationId: number; + notifyOnFailure: boolean; + notifyOnStart: boolean; + notifyOnSuccess: boolean; + notifyOnWarning: boolean; + }>; + }; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/backups/{shortId}/notifications'; }; export type UpdateScheduleNotificationsResponses = { - /** - * Notification assignments updated successfully - */ - 200: Array<{ - createdAt: number; - destination: { - config: - | { - apiToken: string; - priority: -1 | 0 | 1; - type: "pushover"; - userKey: string; - devices?: string; - } - | { - botToken: string; - chatId: string; - type: "telegram"; - threadId?: string; - } - | { - from: string; - smtpHost: string; - smtpPort: number; - to: Array; - type: "email"; - useTLS: boolean; - fromName?: string; - password?: string; - username?: string; - } - | { - method: "GET" | "POST"; - type: "generic"; - url: string; - contentType?: string; - headers?: Array; - messageKey?: string; - titleKey?: string; - useJson?: boolean; - } - | { - priority: "default" | "high" | "low" | "max" | "min"; - topic: string; - type: "ntfy"; - accessToken?: string; - password?: string; - serverUrl?: string; - username?: string; - } - | { - priority: number; - serverUrl: string; - token: string; - type: "gotify"; - path?: string; - } - | { - shoutrrrUrl: string; - type: "custom"; - } - | { - type: "discord"; - webhookUrl: string; - avatarUrl?: string; - threadId?: string; - username?: string; - } - | { - type: "slack"; - webhookUrl: string; - channel?: string; - iconEmoji?: string; - username?: string; - }; - createdAt: number; - enabled: boolean; - id: number; - name: string; - type: "custom" | "discord" | "email" | "generic" | "gotify" | "ntfy" | "pushover" | "slack" | "telegram"; - updatedAt: number; - }; - destinationId: number; - notifyOnFailure: boolean; - notifyOnStart: boolean; - notifyOnSuccess: boolean; - notifyOnWarning: boolean; - scheduleId: number; - }>; + /** + * Notification assignments updated successfully + */ + 200: Array<{ + createdAt: number; + destination: { + config: { + apiToken: string; + priority: -1 | 0 | 1; + type: 'pushover'; + userKey: string; + devices?: string; + } | { + botToken: string; + chatId: string; + type: 'telegram'; + threadId?: string; + } | { + from: string; + smtpHost: string; + smtpPort: number; + to: Array; + type: 'email'; + useTLS: boolean; + fromName?: string; + password?: string; + username?: string; + } | { + method: 'GET' | 'POST'; + type: 'generic'; + url: string; + contentType?: string; + headers?: Array; + messageKey?: string; + titleKey?: string; + useJson?: boolean; + } | { + priority: 'default' | 'high' | 'low' | 'max' | 'min'; + topic: string; + type: 'ntfy'; + accessToken?: string; + password?: string; + serverUrl?: string; + username?: string; + } | { + priority: number; + serverUrl: string; + token: string; + type: 'gotify'; + path?: string; + } | { + shoutrrrUrl: string; + type: 'custom'; + } | { + type: 'discord'; + webhookUrl: string; + avatarUrl?: string; + threadId?: string; + username?: string; + } | { + type: 'slack'; + webhookUrl: string; + channel?: string; + iconEmoji?: string; + username?: string; + }; + createdAt: number; + enabled: boolean; + id: number; + name: string; + type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; + updatedAt: number; + }; + destinationId: number; + notifyOnFailure: boolean; + notifyOnStart: boolean; + notifyOnSuccess: boolean; + notifyOnWarning: boolean; + scheduleId: number; + }>; }; -export type UpdateScheduleNotificationsResponse = - UpdateScheduleNotificationsResponses[keyof UpdateScheduleNotificationsResponses]; +export type UpdateScheduleNotificationsResponse = UpdateScheduleNotificationsResponses[keyof UpdateScheduleNotificationsResponses]; export type GetScheduleMirrorsData = { - body?: never; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/backups/{shortId}/mirrors"; + body?: never; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/backups/{shortId}/mirrors'; }; export type GetScheduleMirrorsResponses = { - /** - * List of mirror repository assignments for the schedule - */ - 200: Array<{ - createdAt: number; - enabled: boolean; - lastCopyAt: number | null; - lastCopyError: string | null; - lastCopyStatus: "error" | "in_progress" | "success" | null; - repository: { - compressionMode: "auto" | "max" | "off" | null; - config: - | { - accessKeyId: string; - backend: "r2"; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - accessKeyId: string; - backend: "s3"; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - accountKey: string; - accountName: string; - backend: "azure"; - container: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - endpointSuffix?: string; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "gcs"; - bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "local"; - path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "rclone"; - path: string; - remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "rest"; - url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - password?: string; - path?: string; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - username?: string; - } - | { - backend: "sftp"; - host: string; - path: string; - privateKey: string; - user: string; - port?: number; - skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - knownHosts?: string; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - }; - createdAt: number; - doctorResult: { - completedAt: number; - steps: Array<{ - error: string | null; - output: string | null; - step: string; - success: boolean; - }>; - success: boolean; - } | null; - id: string; - lastChecked: number | null; - lastError: string | null; - name: string; - shortId: string; - status: "cancelled" | "doctor" | "error" | "healthy" | "unknown" | null; - type: "azure" | "gcs" | "local" | "r2" | "rclone" | "rest" | "s3" | "sftp"; - updatedAt: number; - }; - repositoryId: string; - scheduleId: string; - }>; + /** + * List of mirror repository assignments for the schedule + */ + 200: Array<{ + createdAt: number; + enabled: boolean; + lastCopyAt: number | null; + lastCopyError: string | null; + lastCopyStatus: 'error' | 'in_progress' | 'success' | null; + repository: { + compressionMode: 'auto' | 'max' | 'off' | null; + config: { + accessKeyId: string; + backend: 'r2'; + bucket: string; + endpoint: string; + secretAccessKey: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + accessKeyId: string; + backend: 's3'; + bucket: string; + endpoint: string; + secretAccessKey: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + accountKey: string; + accountName: string; + backend: 'azure'; + container: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + endpointSuffix?: string; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'gcs'; + bucket: string; + credentialsJson: string; + projectId: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'local'; + path: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'rclone'; + path: string; + remote: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'rest'; + url: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + password?: string; + path?: string; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + username?: string; + } | { + backend: 'sftp'; + host: string; + path: string; + privateKey: string; + user: string; + port?: number; + skipHostKeyCheck?: boolean; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + knownHosts?: string; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + }; + createdAt: number; + doctorResult: { + completedAt: number; + steps: Array<{ + error: string | null; + output: string | null; + step: string; + success: boolean; + }>; + success: boolean; + } | null; + id: string; + lastChecked: number | null; + lastError: string | null; + name: string; + shortId: string; + status: 'cancelled' | 'doctor' | 'error' | 'healthy' | 'unknown' | null; + type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; + updatedAt: number; + }; + repositoryId: string; + scheduleId: string; + }>; }; export type GetScheduleMirrorsResponse = GetScheduleMirrorsResponses[keyof GetScheduleMirrorsResponses]; export type UpdateScheduleMirrorsData = { - body?: { - mirrors: Array<{ - enabled: boolean; - repositoryId: string; - }>; - }; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/backups/{shortId}/mirrors"; + body?: { + mirrors: Array<{ + enabled: boolean; + repositoryId: string; + }>; + }; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/backups/{shortId}/mirrors'; }; export type UpdateScheduleMirrorsResponses = { - /** - * Mirror assignments updated successfully - */ - 200: Array<{ - createdAt: number; - enabled: boolean; - lastCopyAt: number | null; - lastCopyError: string | null; - lastCopyStatus: "error" | "in_progress" | "success" | null; - repository: { - compressionMode: "auto" | "max" | "off" | null; - config: - | { - accessKeyId: string; - backend: "r2"; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - accessKeyId: string; - backend: "s3"; - bucket: string; - endpoint: string; - secretAccessKey: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - accountKey: string; - accountName: string; - backend: "azure"; - container: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - endpointSuffix?: string; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "gcs"; - bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "local"; - path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "rclone"; - path: string; - remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - } - | { - backend: "rest"; - url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - password?: string; - path?: string; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - username?: string; - } - | { - backend: "sftp"; - host: string; - path: string; - privateKey: string; - user: string; - port?: number; - skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; - knownHosts?: string; - uploadLimit?: { - unit?: "Gbps" | "Kbps" | "Mbps"; - value?: number; - enabled?: boolean; - }; - }; - createdAt: number; - doctorResult: { - completedAt: number; - steps: Array<{ - error: string | null; - output: string | null; - step: string; - success: boolean; - }>; - success: boolean; - } | null; - id: string; - lastChecked: number | null; - lastError: string | null; - name: string; - shortId: string; - status: "cancelled" | "doctor" | "error" | "healthy" | "unknown" | null; - type: "azure" | "gcs" | "local" | "r2" | "rclone" | "rest" | "s3" | "sftp"; - updatedAt: number; - }; - repositoryId: string; - scheduleId: string; - }>; + /** + * Mirror assignments updated successfully + */ + 200: Array<{ + createdAt: number; + enabled: boolean; + lastCopyAt: number | null; + lastCopyError: string | null; + lastCopyStatus: 'error' | 'in_progress' | 'success' | null; + repository: { + compressionMode: 'auto' | 'max' | 'off' | null; + config: { + accessKeyId: string; + backend: 'r2'; + bucket: string; + endpoint: string; + secretAccessKey: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + accessKeyId: string; + backend: 's3'; + bucket: string; + endpoint: string; + secretAccessKey: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + accountKey: string; + accountName: string; + backend: 'azure'; + container: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + endpointSuffix?: string; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'gcs'; + bucket: string; + credentialsJson: string; + projectId: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'local'; + path: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'rclone'; + path: string; + remote: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + } | { + backend: 'rest'; + url: string; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + password?: string; + path?: string; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + username?: string; + } | { + backend: 'sftp'; + host: string; + path: string; + privateKey: string; + user: string; + port?: number; + skipHostKeyCheck?: boolean; + cacert?: string; + customPassword?: string; + downloadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + insecureTls?: boolean; + isExistingRepository?: boolean; + knownHosts?: string; + uploadLimit?: { + unit?: 'Gbps' | 'Kbps' | 'Mbps'; + value?: number; + enabled?: boolean; + }; + }; + createdAt: number; + doctorResult: { + completedAt: number; + steps: Array<{ + error: string | null; + output: string | null; + step: string; + success: boolean; + }>; + success: boolean; + } | null; + id: string; + lastChecked: number | null; + lastError: string | null; + name: string; + shortId: string; + status: 'cancelled' | 'doctor' | 'error' | 'healthy' | 'unknown' | null; + type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; + updatedAt: number; + }; + repositoryId: string; + scheduleId: string; + }>; }; export type UpdateScheduleMirrorsResponse = UpdateScheduleMirrorsResponses[keyof UpdateScheduleMirrorsResponses]; export type GetMirrorCompatibilityData = { - body?: never; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/backups/{shortId}/mirrors/compatibility"; + body?: never; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/backups/{shortId}/mirrors/compatibility'; }; export type GetMirrorCompatibilityResponses = { - /** - * List of repositories with their mirror compatibility status - */ - 200: Array<{ - compatible: boolean; - reason: string | null; - repositoryId: string; - }>; + /** + * List of repositories with their mirror compatibility status + */ + 200: Array<{ + compatible: boolean; + reason: string | null; + repositoryId: string; + }>; }; export type GetMirrorCompatibilityResponse = GetMirrorCompatibilityResponses[keyof GetMirrorCompatibilityResponses]; export type ReorderBackupSchedulesData = { - body?: { - scheduleShortIds: Array; - }; - path?: never; - query?: never; - url: "/api/v1/backups/reorder"; + body?: { + scheduleShortIds: Array; + }; + path?: never; + query?: never; + url: '/api/v1/backups/reorder'; }; export type ReorderBackupSchedulesResponses = { - /** - * Backup schedules reordered successfully - */ - 200: { - success: boolean; - }; + /** + * Backup schedules reordered successfully + */ + 200: { + success: boolean; + }; }; export type ReorderBackupSchedulesResponse = ReorderBackupSchedulesResponses[keyof ReorderBackupSchedulesResponses]; export type GetBackupProgressData = { - body?: never; - path: { - shortId: string; - }; - query?: never; - url: "/api/v1/backups/{shortId}/progress"; + body?: never; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/backups/{shortId}/progress'; }; export type GetBackupProgressResponses = { - /** - * Current backup progress or null if not yet available - */ - 200: { - bytes_done: number; - files_done: number; - percent_done: number; - repositoryName: string; - scheduleId: string; - seconds_elapsed: number; - total_bytes: number; - total_files: number; - volumeName: string; - current_files?: Array; - } | null; + /** + * Current backup progress or null if not yet available + */ + 200: { + bytes_done: number; + files_done: number; + percent_done: number; + repositoryName: string; + scheduleId: string; + seconds_elapsed: number; + total_bytes: number; + total_files: number; + volumeName: string; + current_files?: Array; + } | null; }; export type GetBackupProgressResponse = GetBackupProgressResponses[keyof GetBackupProgressResponses]; export type ListNotificationDestinationsData = { - body?: never; - path?: never; - query?: never; - url: "/api/v1/notifications/destinations"; + body?: never; + path?: never; + query?: never; + url: '/api/v1/notifications/destinations'; }; export type ListNotificationDestinationsResponses = { - /** - * A list of notification destinations - */ - 200: Array<{ - config: - | { - apiToken: string; - priority: -1 | 0 | 1; - type: "pushover"; - userKey: string; - devices?: string; - } - | { - botToken: string; - chatId: string; - type: "telegram"; - threadId?: string; - } - | { - from: string; - smtpHost: string; - smtpPort: number; - to: Array; - type: "email"; - useTLS: boolean; - fromName?: string; - password?: string; - username?: string; - } - | { - method: "GET" | "POST"; - type: "generic"; - url: string; - contentType?: string; - headers?: Array; - messageKey?: string; - titleKey?: string; - useJson?: boolean; - } - | { - priority: "default" | "high" | "low" | "max" | "min"; - topic: string; - type: "ntfy"; - accessToken?: string; - password?: string; - serverUrl?: string; - username?: string; - } - | { - priority: number; - serverUrl: string; - token: string; - type: "gotify"; - path?: string; - } - | { - shoutrrrUrl: string; - type: "custom"; - } - | { - type: "discord"; - webhookUrl: string; - avatarUrl?: string; - threadId?: string; - username?: string; - } - | { - type: "slack"; - webhookUrl: string; - channel?: string; - iconEmoji?: string; - username?: string; - }; - createdAt: number; - enabled: boolean; - id: number; - name: string; - type: "custom" | "discord" | "email" | "generic" | "gotify" | "ntfy" | "pushover" | "slack" | "telegram"; - updatedAt: number; - }>; + /** + * A list of notification destinations + */ + 200: Array<{ + config: { + apiToken: string; + priority: -1 | 0 | 1; + type: 'pushover'; + userKey: string; + devices?: string; + } | { + botToken: string; + chatId: string; + type: 'telegram'; + threadId?: string; + } | { + from: string; + smtpHost: string; + smtpPort: number; + to: Array; + type: 'email'; + useTLS: boolean; + fromName?: string; + password?: string; + username?: string; + } | { + method: 'GET' | 'POST'; + type: 'generic'; + url: string; + contentType?: string; + headers?: Array; + messageKey?: string; + titleKey?: string; + useJson?: boolean; + } | { + priority: 'default' | 'high' | 'low' | 'max' | 'min'; + topic: string; + type: 'ntfy'; + accessToken?: string; + password?: string; + serverUrl?: string; + username?: string; + } | { + priority: number; + serverUrl: string; + token: string; + type: 'gotify'; + path?: string; + } | { + shoutrrrUrl: string; + type: 'custom'; + } | { + type: 'discord'; + webhookUrl: string; + avatarUrl?: string; + threadId?: string; + username?: string; + } | { + type: 'slack'; + webhookUrl: string; + channel?: string; + iconEmoji?: string; + username?: string; + }; + createdAt: number; + enabled: boolean; + id: number; + name: string; + type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; + updatedAt: number; + }>; }; -export type ListNotificationDestinationsResponse = - ListNotificationDestinationsResponses[keyof ListNotificationDestinationsResponses]; +export type ListNotificationDestinationsResponse = ListNotificationDestinationsResponses[keyof ListNotificationDestinationsResponses]; export type CreateNotificationDestinationData = { - body?: { - config: - | { - apiToken: string; - priority: -1 | 0 | 1; - type: "pushover"; - userKey: string; - devices?: string; - } - | { - botToken: string; - chatId: string; - type: "telegram"; - threadId?: string; - } - | { - from: string; - smtpHost: string; - smtpPort: number; - to: Array; - type: "email"; - useTLS: boolean; - fromName?: string; - password?: string; - username?: string; - } - | { - method: "GET" | "POST"; - type: "generic"; - url: string; - contentType?: string; - headers?: Array; - messageKey?: string; - titleKey?: string; - useJson?: boolean; - } - | { - priority: "default" | "high" | "low" | "max" | "min"; - topic: string; - type: "ntfy"; - accessToken?: string; - password?: string; - serverUrl?: string; - username?: string; - } - | { - priority: number; - serverUrl: string; - token: string; - type: "gotify"; - path?: string; - } - | { - shoutrrrUrl: string; - type: "custom"; - } - | { - type: "discord"; - webhookUrl: string; - avatarUrl?: string; - threadId?: string; - username?: string; - } - | { - type: "slack"; - webhookUrl: string; - channel?: string; - iconEmoji?: string; - username?: string; - }; - name: string; - }; - path?: never; - query?: never; - url: "/api/v1/notifications/destinations"; + body?: { + config: { + apiToken: string; + priority: -1 | 0 | 1; + type: 'pushover'; + userKey: string; + devices?: string; + } | { + botToken: string; + chatId: string; + type: 'telegram'; + threadId?: string; + } | { + from: string; + smtpHost: string; + smtpPort: number; + to: Array; + type: 'email'; + useTLS: boolean; + fromName?: string; + password?: string; + username?: string; + } | { + method: 'GET' | 'POST'; + type: 'generic'; + url: string; + contentType?: string; + headers?: Array; + messageKey?: string; + titleKey?: string; + useJson?: boolean; + } | { + priority: 'default' | 'high' | 'low' | 'max' | 'min'; + topic: string; + type: 'ntfy'; + accessToken?: string; + password?: string; + serverUrl?: string; + username?: string; + } | { + priority: number; + serverUrl: string; + token: string; + type: 'gotify'; + path?: string; + } | { + shoutrrrUrl: string; + type: 'custom'; + } | { + type: 'discord'; + webhookUrl: string; + avatarUrl?: string; + threadId?: string; + username?: string; + } | { + type: 'slack'; + webhookUrl: string; + channel?: string; + iconEmoji?: string; + username?: string; + }; + name: string; + }; + path?: never; + query?: never; + url: '/api/v1/notifications/destinations'; }; export type CreateNotificationDestinationResponses = { - /** - * Notification destination created successfully - */ - 201: { - config: - | { - apiToken: string; - priority: -1 | 0 | 1; - type: "pushover"; - userKey: string; - devices?: string; - } - | { - botToken: string; - chatId: string; - type: "telegram"; - threadId?: string; - } - | { - from: string; - smtpHost: string; - smtpPort: number; - to: Array; - type: "email"; - useTLS: boolean; - fromName?: string; - password?: string; - username?: string; - } - | { - method: "GET" | "POST"; - type: "generic"; - url: string; - contentType?: string; - headers?: Array; - messageKey?: string; - titleKey?: string; - useJson?: boolean; - } - | { - priority: "default" | "high" | "low" | "max" | "min"; - topic: string; - type: "ntfy"; - accessToken?: string; - password?: string; - serverUrl?: string; - username?: string; - } - | { - priority: number; - serverUrl: string; - token: string; - type: "gotify"; - path?: string; - } - | { - shoutrrrUrl: string; - type: "custom"; - } - | { - type: "discord"; - webhookUrl: string; - avatarUrl?: string; - threadId?: string; - username?: string; - } - | { - type: "slack"; - webhookUrl: string; - channel?: string; - iconEmoji?: string; - username?: string; - }; - createdAt: number; - enabled: boolean; - id: number; - name: string; - type: "custom" | "discord" | "email" | "generic" | "gotify" | "ntfy" | "pushover" | "slack" | "telegram"; - updatedAt: number; - }; + /** + * Notification destination created successfully + */ + 201: { + config: { + apiToken: string; + priority: -1 | 0 | 1; + type: 'pushover'; + userKey: string; + devices?: string; + } | { + botToken: string; + chatId: string; + type: 'telegram'; + threadId?: string; + } | { + from: string; + smtpHost: string; + smtpPort: number; + to: Array; + type: 'email'; + useTLS: boolean; + fromName?: string; + password?: string; + username?: string; + } | { + method: 'GET' | 'POST'; + type: 'generic'; + url: string; + contentType?: string; + headers?: Array; + messageKey?: string; + titleKey?: string; + useJson?: boolean; + } | { + priority: 'default' | 'high' | 'low' | 'max' | 'min'; + topic: string; + type: 'ntfy'; + accessToken?: string; + password?: string; + serverUrl?: string; + username?: string; + } | { + priority: number; + serverUrl: string; + token: string; + type: 'gotify'; + path?: string; + } | { + shoutrrrUrl: string; + type: 'custom'; + } | { + type: 'discord'; + webhookUrl: string; + avatarUrl?: string; + threadId?: string; + username?: string; + } | { + type: 'slack'; + webhookUrl: string; + channel?: string; + iconEmoji?: string; + username?: string; + }; + createdAt: number; + enabled: boolean; + id: number; + name: string; + type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; + updatedAt: number; + }; }; -export type CreateNotificationDestinationResponse = - CreateNotificationDestinationResponses[keyof CreateNotificationDestinationResponses]; +export type CreateNotificationDestinationResponse = CreateNotificationDestinationResponses[keyof CreateNotificationDestinationResponses]; export type DeleteNotificationDestinationData = { - body?: never; - path: { - id: string; - }; - query?: never; - url: "/api/v1/notifications/destinations/{id}"; + body?: never; + path: { + id: string; + }; + query?: never; + url: '/api/v1/notifications/destinations/{id}'; }; export type DeleteNotificationDestinationErrors = { - /** - * Notification destination not found - */ - 404: unknown; + /** + * Notification destination not found + */ + 404: unknown; }; export type DeleteNotificationDestinationResponses = { - /** - * Notification destination deleted successfully - */ - 200: { - message: string; - }; + /** + * Notification destination deleted successfully + */ + 200: { + message: string; + }; }; -export type DeleteNotificationDestinationResponse = - DeleteNotificationDestinationResponses[keyof DeleteNotificationDestinationResponses]; +export type DeleteNotificationDestinationResponse = DeleteNotificationDestinationResponses[keyof DeleteNotificationDestinationResponses]; export type GetNotificationDestinationData = { - body?: never; - path: { - id: string; - }; - query?: never; - url: "/api/v1/notifications/destinations/{id}"; + body?: never; + path: { + id: string; + }; + query?: never; + url: '/api/v1/notifications/destinations/{id}'; }; export type GetNotificationDestinationErrors = { - /** - * Notification destination not found - */ - 404: unknown; + /** + * Notification destination not found + */ + 404: unknown; }; export type GetNotificationDestinationResponses = { - /** - * Notification destination details - */ - 200: { - config: - | { - apiToken: string; - priority: -1 | 0 | 1; - type: "pushover"; - userKey: string; - devices?: string; - } - | { - botToken: string; - chatId: string; - type: "telegram"; - threadId?: string; - } - | { - from: string; - smtpHost: string; - smtpPort: number; - to: Array; - type: "email"; - useTLS: boolean; - fromName?: string; - password?: string; - username?: string; - } - | { - method: "GET" | "POST"; - type: "generic"; - url: string; - contentType?: string; - headers?: Array; - messageKey?: string; - titleKey?: string; - useJson?: boolean; - } - | { - priority: "default" | "high" | "low" | "max" | "min"; - topic: string; - type: "ntfy"; - accessToken?: string; - password?: string; - serverUrl?: string; - username?: string; - } - | { - priority: number; - serverUrl: string; - token: string; - type: "gotify"; - path?: string; - } - | { - shoutrrrUrl: string; - type: "custom"; - } - | { - type: "discord"; - webhookUrl: string; - avatarUrl?: string; - threadId?: string; - username?: string; - } - | { - type: "slack"; - webhookUrl: string; - channel?: string; - iconEmoji?: string; - username?: string; - }; - createdAt: number; - enabled: boolean; - id: number; - name: string; - type: "custom" | "discord" | "email" | "generic" | "gotify" | "ntfy" | "pushover" | "slack" | "telegram"; - updatedAt: number; - }; + /** + * Notification destination details + */ + 200: { + config: { + apiToken: string; + priority: -1 | 0 | 1; + type: 'pushover'; + userKey: string; + devices?: string; + } | { + botToken: string; + chatId: string; + type: 'telegram'; + threadId?: string; + } | { + from: string; + smtpHost: string; + smtpPort: number; + to: Array; + type: 'email'; + useTLS: boolean; + fromName?: string; + password?: string; + username?: string; + } | { + method: 'GET' | 'POST'; + type: 'generic'; + url: string; + contentType?: string; + headers?: Array; + messageKey?: string; + titleKey?: string; + useJson?: boolean; + } | { + priority: 'default' | 'high' | 'low' | 'max' | 'min'; + topic: string; + type: 'ntfy'; + accessToken?: string; + password?: string; + serverUrl?: string; + username?: string; + } | { + priority: number; + serverUrl: string; + token: string; + type: 'gotify'; + path?: string; + } | { + shoutrrrUrl: string; + type: 'custom'; + } | { + type: 'discord'; + webhookUrl: string; + avatarUrl?: string; + threadId?: string; + username?: string; + } | { + type: 'slack'; + webhookUrl: string; + channel?: string; + iconEmoji?: string; + username?: string; + }; + createdAt: number; + enabled: boolean; + id: number; + name: string; + type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; + updatedAt: number; + }; }; -export type GetNotificationDestinationResponse = - GetNotificationDestinationResponses[keyof GetNotificationDestinationResponses]; +export type GetNotificationDestinationResponse = GetNotificationDestinationResponses[keyof GetNotificationDestinationResponses]; export type UpdateNotificationDestinationData = { - body?: { - config?: - | { - apiToken: string; - priority: -1 | 0 | 1; - type: "pushover"; - userKey: string; - devices?: string; - } - | { - botToken: string; - chatId: string; - type: "telegram"; - threadId?: string; - } - | { - from: string; - smtpHost: string; - smtpPort: number; - to: Array; - type: "email"; - useTLS: boolean; - fromName?: string; - password?: string; - username?: string; - } - | { - method: "GET" | "POST"; - type: "generic"; - url: string; - contentType?: string; - headers?: Array; - messageKey?: string; - titleKey?: string; - useJson?: boolean; - } - | { - priority: "default" | "high" | "low" | "max" | "min"; - topic: string; - type: "ntfy"; - accessToken?: string; - password?: string; - serverUrl?: string; - username?: string; - } - | { - priority: number; - serverUrl: string; - token: string; - type: "gotify"; - path?: string; - } - | { - shoutrrrUrl: string; - type: "custom"; - } - | { - type: "discord"; - webhookUrl: string; - avatarUrl?: string; - threadId?: string; - username?: string; - } - | { - type: "slack"; - webhookUrl: string; - channel?: string; - iconEmoji?: string; - username?: string; - }; - enabled?: boolean; - name?: string; - }; - path: { - id: string; - }; - query?: never; - url: "/api/v1/notifications/destinations/{id}"; + body?: { + config?: { + apiToken: string; + priority: -1 | 0 | 1; + type: 'pushover'; + userKey: string; + devices?: string; + } | { + botToken: string; + chatId: string; + type: 'telegram'; + threadId?: string; + } | { + from: string; + smtpHost: string; + smtpPort: number; + to: Array; + type: 'email'; + useTLS: boolean; + fromName?: string; + password?: string; + username?: string; + } | { + method: 'GET' | 'POST'; + type: 'generic'; + url: string; + contentType?: string; + headers?: Array; + messageKey?: string; + titleKey?: string; + useJson?: boolean; + } | { + priority: 'default' | 'high' | 'low' | 'max' | 'min'; + topic: string; + type: 'ntfy'; + accessToken?: string; + password?: string; + serverUrl?: string; + username?: string; + } | { + priority: number; + serverUrl: string; + token: string; + type: 'gotify'; + path?: string; + } | { + shoutrrrUrl: string; + type: 'custom'; + } | { + type: 'discord'; + webhookUrl: string; + avatarUrl?: string; + threadId?: string; + username?: string; + } | { + type: 'slack'; + webhookUrl: string; + channel?: string; + iconEmoji?: string; + username?: string; + }; + enabled?: boolean; + name?: string; + }; + path: { + id: string; + }; + query?: never; + url: '/api/v1/notifications/destinations/{id}'; }; export type UpdateNotificationDestinationErrors = { - /** - * Notification destination not found - */ - 404: unknown; + /** + * Notification destination not found + */ + 404: unknown; }; export type UpdateNotificationDestinationResponses = { - /** - * Notification destination updated successfully - */ - 200: { - config: - | { - apiToken: string; - priority: -1 | 0 | 1; - type: "pushover"; - userKey: string; - devices?: string; - } - | { - botToken: string; - chatId: string; - type: "telegram"; - threadId?: string; - } - | { - from: string; - smtpHost: string; - smtpPort: number; - to: Array; - type: "email"; - useTLS: boolean; - fromName?: string; - password?: string; - username?: string; - } - | { - method: "GET" | "POST"; - type: "generic"; - url: string; - contentType?: string; - headers?: Array; - messageKey?: string; - titleKey?: string; - useJson?: boolean; - } - | { - priority: "default" | "high" | "low" | "max" | "min"; - topic: string; - type: "ntfy"; - accessToken?: string; - password?: string; - serverUrl?: string; - username?: string; - } - | { - priority: number; - serverUrl: string; - token: string; - type: "gotify"; - path?: string; - } - | { - shoutrrrUrl: string; - type: "custom"; - } - | { - type: "discord"; - webhookUrl: string; - avatarUrl?: string; - threadId?: string; - username?: string; - } - | { - type: "slack"; - webhookUrl: string; - channel?: string; - iconEmoji?: string; - username?: string; - }; - createdAt: number; - enabled: boolean; - id: number; - name: string; - type: "custom" | "discord" | "email" | "generic" | "gotify" | "ntfy" | "pushover" | "slack" | "telegram"; - updatedAt: number; - }; + /** + * Notification destination updated successfully + */ + 200: { + config: { + apiToken: string; + priority: -1 | 0 | 1; + type: 'pushover'; + userKey: string; + devices?: string; + } | { + botToken: string; + chatId: string; + type: 'telegram'; + threadId?: string; + } | { + from: string; + smtpHost: string; + smtpPort: number; + to: Array; + type: 'email'; + useTLS: boolean; + fromName?: string; + password?: string; + username?: string; + } | { + method: 'GET' | 'POST'; + type: 'generic'; + url: string; + contentType?: string; + headers?: Array; + messageKey?: string; + titleKey?: string; + useJson?: boolean; + } | { + priority: 'default' | 'high' | 'low' | 'max' | 'min'; + topic: string; + type: 'ntfy'; + accessToken?: string; + password?: string; + serverUrl?: string; + username?: string; + } | { + priority: number; + serverUrl: string; + token: string; + type: 'gotify'; + path?: string; + } | { + shoutrrrUrl: string; + type: 'custom'; + } | { + type: 'discord'; + webhookUrl: string; + avatarUrl?: string; + threadId?: string; + username?: string; + } | { + type: 'slack'; + webhookUrl: string; + channel?: string; + iconEmoji?: string; + username?: string; + }; + createdAt: number; + enabled: boolean; + id: number; + name: string; + type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; + updatedAt: number; + }; }; -export type UpdateNotificationDestinationResponse = - UpdateNotificationDestinationResponses[keyof UpdateNotificationDestinationResponses]; +export type UpdateNotificationDestinationResponse = UpdateNotificationDestinationResponses[keyof UpdateNotificationDestinationResponses]; export type TestNotificationDestinationData = { - body?: never; - path: { - id: string; - }; - query?: never; - url: "/api/v1/notifications/destinations/{id}/test"; + body?: never; + path: { + id: string; + }; + query?: never; + url: '/api/v1/notifications/destinations/{id}/test'; }; export type TestNotificationDestinationErrors = { - /** - * Notification destination not found - */ - 404: unknown; - /** - * Cannot test disabled destination - */ - 409: unknown; - /** - * Failed to send test notification - */ - 500: unknown; + /** + * Notification destination not found + */ + 404: unknown; + /** + * Cannot test disabled destination + */ + 409: unknown; + /** + * Failed to send test notification + */ + 500: unknown; }; export type TestNotificationDestinationResponses = { - /** - * Test notification sent successfully - */ - 200: { - success: boolean; - }; + /** + * Test notification sent successfully + */ + 200: { + success: boolean; + }; }; -export type TestNotificationDestinationResponse = - TestNotificationDestinationResponses[keyof TestNotificationDestinationResponses]; +export type TestNotificationDestinationResponse = TestNotificationDestinationResponses[keyof TestNotificationDestinationResponses]; export type GetSystemInfoData = { - body?: never; - path?: never; - query?: never; - url: "/api/v1/system/info"; + body?: never; + path?: never; + query?: never; + url: '/api/v1/system/info'; }; export type GetSystemInfoResponses = { - /** - * System information with enabled capabilities - */ - 200: { - capabilities: { - rclone: boolean; - sysAdmin: boolean; - }; - }; + /** + * System information with enabled capabilities + */ + 200: { + capabilities: { + rclone: boolean; + sysAdmin: boolean; + }; + }; }; export type GetSystemInfoResponse = GetSystemInfoResponses[keyof GetSystemInfoResponses]; export type GetUpdatesData = { - body?: never; - path?: never; - query?: never; - url: "/api/v1/system/updates"; + body?: never; + path?: never; + query?: never; + url: '/api/v1/system/updates'; }; export type GetUpdatesResponses = { - /** - * Update information and missed releases - */ - 200: { - currentVersion: string; - hasUpdate: boolean; - latestVersion: string; - missedReleases: Array<{ - body: string; - publishedAt: string; - url: string; - version: string; - }>; - }; + /** + * Update information and missed releases + */ + 200: { + currentVersion: string; + hasUpdate: boolean; + latestVersion: string; + missedReleases: Array<{ + body: string; + publishedAt: string; + url: string; + version: string; + }>; + }; }; export type GetUpdatesResponse = GetUpdatesResponses[keyof GetUpdatesResponses]; export type GetRegistrationStatusData = { - body?: never; - path?: never; - query?: never; - url: "/api/v1/system/registration-status"; + body?: never; + path?: never; + query?: never; + url: '/api/v1/system/registration-status'; }; export type GetRegistrationStatusResponses = { - /** - * Registration status - */ - 200: { - enabled: boolean; - }; + /** + * Registration status + */ + 200: { + enabled: boolean; + }; }; export type GetRegistrationStatusResponse = GetRegistrationStatusResponses[keyof GetRegistrationStatusResponses]; export type SetRegistrationStatusData = { - body?: { - enabled: boolean; - }; - path?: never; - query?: never; - url: "/api/v1/system/registration-status"; + body?: { + enabled: boolean; + }; + path?: never; + query?: never; + url: '/api/v1/system/registration-status'; }; export type SetRegistrationStatusResponses = { - /** - * Registration status updated - */ - 200: { - enabled: boolean; - }; + /** + * Registration status updated + */ + 200: { + enabled: boolean; + }; }; export type SetRegistrationStatusResponse = SetRegistrationStatusResponses[keyof SetRegistrationStatusResponses]; export type DownloadResticPasswordData = { - body?: { - password: string; - }; - path?: never; - query?: never; - url: "/api/v1/system/restic-password"; + body?: { + password: string; + }; + path?: never; + query?: never; + url: '/api/v1/system/restic-password'; }; export type DownloadResticPasswordResponses = { - /** - * Organization's Restic password - */ - 200: string; + /** + * Organization's Restic password + */ + 200: string; }; export type DownloadResticPasswordResponse = DownloadResticPasswordResponses[keyof DownloadResticPasswordResponses]; export type GetDevPanelData = { - body?: never; - path?: never; - query?: never; - url: "/api/v1/system/dev-panel"; + body?: never; + path?: never; + query?: never; + url: '/api/v1/system/dev-panel'; }; export type GetDevPanelResponses = { - /** - * Dev panel status - */ - 200: { - enabled: boolean; - }; + /** + * Dev panel status + */ + 200: { + enabled: boolean; + }; }; export type GetDevPanelResponse = GetDevPanelResponses[keyof GetDevPanelResponses]; diff --git a/app/client/components/app-sidebar.tsx b/app/client/components/app-sidebar.tsx index 6c5f301f..14e2d700 100644 --- a/app/client/components/app-sidebar.tsx +++ b/app/client/components/app-sidebar.tsx @@ -18,6 +18,7 @@ import { cn } from "~/client/lib/utils"; import { APP_VERSION, RCLONE_VERSION, RESTIC_VERSION, SHOUTRRR_VERSION } from "~/client/lib/version"; import { useUpdates } from "~/client/hooks/use-updates"; import { ReleaseNotesDialog } from "./release-notes-dialog"; +import { OrganizationSwitcher } from "./organization-switcher"; import { Link } from "@tanstack/react-router"; const items = [ @@ -131,7 +132,8 @@ export function AppSidebar() { - + +
diff --git a/app/client/components/grid-background.tsx b/app/client/components/grid-background.tsx index 5c46461e..ec417f3a 100644 --- a/app/client/components/grid-background.tsx +++ b/app/client/components/grid-background.tsx @@ -16,7 +16,7 @@ export function GridBackground({ children, className, containerClassName }: Grid "bg-[size:40px_40px]", "bg-[linear-gradient(to_right,#e4e4e7_1px,transparent_1px),linear-gradient(to_bottom,#e4e4e7_1px,transparent_1px)]", "dark:bg-[linear-gradient(to_right,#262626_1px,transparent_1px),linear-gradient(to_bottom,#262626_1px,transparent_1px)]", - "[mask-image:radial-gradient(ellipse_at_top,black_40%,transparent_100%)]", + "[mask-image:radial-gradient(ellipse_at_top,black_70%,transparent_100%)]", )} />
{children}
diff --git a/app/client/components/layout.tsx b/app/client/components/layout.tsx index 804c0c36..f7c0a7a5 100644 --- a/app/client/components/layout.tsx +++ b/app/client/components/layout.tsx @@ -44,7 +44,7 @@ export function Layout({ loaderData }: Props) { {loaderData.user && (
- {loaderData.user?.username} + {loaderData.user.name} diff --git a/app/client/components/organization-switcher.tsx b/app/client/components/organization-switcher.tsx new file mode 100644 index 00000000..91f3f0fa --- /dev/null +++ b/app/client/components/organization-switcher.tsx @@ -0,0 +1,110 @@ +import { ChevronsUpDown } from "lucide-react"; +import { toast } from "sonner"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "~/client/components/ui/dropdown-menu"; +import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from "~/client/components/ui/sidebar"; +import { authClient } from "~/client/lib/auth-client"; +import { useMutation } from "@tanstack/react-query"; +import { useOrganizationContext } from "../hooks/use-org-context"; + +function getOrganizationInitials(name?: string): string { + const trimmedName = name?.trim(); + + if (!trimmedName) { + return "O"; + } + + return trimmedName + .split(/\s+/) + .slice(0, 2) + .map((part) => part.charAt(0).toUpperCase()) + .join(""); +} + +export function OrganizationSwitcher() { + const { isMobile } = useSidebar(); + const { organizations, activeOrganization } = useOrganizationContext(); + + const switchOrganizationMutation = useMutation({ + mutationFn: async (organizationId: string) => { + const { error } = await authClient.organization.setActive({ organizationId }); + if (error) throw new Error(error.message); + }, + onError: (error) => { + const message = error instanceof Error ? error.message : "Unexpected error while switching organizations"; + toast.error("Failed to switch organization", { description: message }); + }, + }); + + if (organizations === undefined) { + return null; + } + + if (organizations.length <= 1) { + return null; + } + + return ( + + + + + +
+ {activeOrganization?.logo ? ( + {`${activeOrganization.name} + ) : ( + {getOrganizationInitials(activeOrganization?.name)} + )} +
+
+ {activeOrganization?.name} + {organizations.length} organizations +
+ +
+
+ + Organizations + {organizations.map((organization) => ( + switchOrganizationMutation.mutate(organization.id)} + className="gap-2 p-2" + disabled={switchOrganizationMutation.isPending} + > +
+ {organization.logo ? ( + {`${organization.name} + ) : ( + {getOrganizationInitials(organization.name)} + )} +
+ {organization.name} + {organization.id === activeOrganization?.id && "Current"} +
+ ))} +
+
+
+
+ ); +} diff --git a/app/client/components/ui/card.tsx b/app/client/components/ui/card.tsx index 1ff9e505..00aa1e72 100644 --- a/app/client/components/ui/card.tsx +++ b/app/client/components/ui/card.tsx @@ -7,7 +7,7 @@ function Card({ className, children, interactive, ...props }: React.ComponentPro
) { + return ; +} + +function DropdownMenuPortal({ ...props }: React.ComponentProps) { + return ; +} + +function DropdownMenuTrigger({ ...props }: React.ComponentProps) { + return ; +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function DropdownMenuGroup({ ...props }: React.ComponentProps) { + return ; +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean; + variant?: "default" | "destructive"; +}) { + return ( + + ); +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuRadioGroup({ ...props }: React.ComponentProps) { + return ; +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + ); +} + +function DropdownMenuSeparator({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) { + return ( + + ); +} + +function DropdownMenuSub({ ...props }: React.ComponentProps) { + return ; +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + {children} + + + ); +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +}; diff --git a/app/client/functions/get-origin.ts b/app/client/functions/get-origin.ts new file mode 100644 index 00000000..c14a7aa5 --- /dev/null +++ b/app/client/functions/get-origin.ts @@ -0,0 +1,6 @@ +import { createIsomorphicFn } from "@tanstack/react-start"; +import { config } from "~/server/core/config"; + +export const getOrigin = createIsomorphicFn() + .server(() => config.baseUrl) + .client(() => window.location.origin); diff --git a/app/client/hooks/use-org-context.ts b/app/client/hooks/use-org-context.ts new file mode 100644 index 00000000..9a6e7686 --- /dev/null +++ b/app/client/hooks/use-org-context.ts @@ -0,0 +1,13 @@ +import { useSuspenseQuery } from "@tanstack/react-query"; +import { useServerFn } from "@tanstack/react-start"; +import { getOrganizationContext } from "~/server/lib/functions/organization-context"; + +export function useOrganizationContext() { + const getOrgContext = useServerFn(getOrganizationContext); + const { data } = useSuspenseQuery({ + queryKey: ["organization-context"], + queryFn: getOrgContext, + }); + + return data; +} diff --git a/app/client/lib/auth-client.ts b/app/client/lib/auth-client.ts index 15744898..aa7d031c 100644 --- a/app/client/lib/auth-client.ts +++ b/app/client/lib/auth-client.ts @@ -6,6 +6,7 @@ import { organizationClient, inferAdditionalFields, } from "better-auth/client/plugins"; +import { ssoClient } from "@better-auth/sso/client"; import type { auth } from "~/server/lib/auth"; export const authClient = createAuthClient({ @@ -14,6 +15,7 @@ export const authClient = createAuthClient({ usernameClient(), adminClient(), organizationClient(), + ssoClient(), twoFactorClient(), ], }); diff --git a/app/client/lib/auth-errors.ts b/app/client/lib/auth-errors.ts new file mode 100644 index 00000000..025ac021 --- /dev/null +++ b/app/client/lib/auth-errors.ts @@ -0,0 +1,62 @@ +export type LoginErrorCode = + | "ACCOUNT_LINK_REQUIRED" + | "EMAIL_NOT_VERIFIED" + | "INVITE_REQUIRED" + | "BANNED_USER" + | "SSO_LOGIN_FAILED"; + +export function decodeLoginError(error?: string): LoginErrorCode | null { + if (!error) { + return null; + } + + let decoded = ""; + + try { + decoded = decodeURIComponent(error); + } catch { + decoded = error; + } + + decoded = decoded.toLowerCase().replace(/[-_\s]+/g, "_"); + + if (decoded.includes("account_not_linked")) { + return "ACCOUNT_LINK_REQUIRED"; + } + + if (decoded.includes("email_not_verified")) { + return "EMAIL_NOT_VERIFIED"; + } + + if (decoded.includes("banned_user") || decoded.includes("banned")) { + return "BANNED_USER"; + } + + if ( + decoded.includes("access_denied") || + decoded.includes("must_be_invited") || + decoded.includes("unable_to_create_session") || + decoded.includes("invite") + ) { + return "INVITE_REQUIRED"; + } + + return "SSO_LOGIN_FAILED"; +} + +export function getLoginErrorDescription(errorCode: LoginErrorCode | null): string | null { + switch (errorCode) { + case "ACCOUNT_LINK_REQUIRED": + return "Your account exists but is not linked to this SSO provider. Sign in with username/password first, then enable auto linking in your provider settings or contact your administrator."; + case "EMAIL_NOT_VERIFIED": + return "Your identity provider did not mark your email as verified."; + case "INVITE_REQUIRED": + return "Access is invite-only. Ask an organization admin to send you an invitation before signing in with SSO."; + case "BANNED_USER": + return "You have been banned from this application. Please contact support if you believe this is an error."; + case "SSO_LOGIN_FAILED": + return "SSO authentication failed. Please try again."; + default: + return null; + } +} diff --git a/app/client/modules/auth/routes/__tests__/login.test.tsx b/app/client/modules/auth/routes/__tests__/login.test.tsx new file mode 100644 index 00000000..f8f56678 --- /dev/null +++ b/app/client/modules/auth/routes/__tests__/login.test.tsx @@ -0,0 +1,57 @@ +import { describe, expect, mock, test } from "bun:test"; +import { render, screen } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +await mock.module("@tanstack/react-router", () => ({ + useNavigate: () => mock(() => {}), +})); + +await mock.module("~/client/api-client/@tanstack/react-query.gen", () => ({ + getPublicSsoProvidersOptions: () => ({ + queryKey: ["public-sso-providers"], + queryFn: async () => ({ providers: [] }), + }), +})); + +await mock.module("~/client/lib/auth-client", () => ({ + authClient: { + getSession: mock(async () => ({ data: null })), + signIn: { + username: mock(async () => ({ data: null, error: null })), + sso: mock(async () => ({ data: null, error: null })), + }, + twoFactor: { + verifyTotp: mock(async () => ({ data: null, error: null })), + }, + }, +})); + +import { LoginPage } from "../login"; + +const createTestQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false } } }); +const inviteOnlyMessage = + "Access is invite-only. Ask an organization admin to send you an invitation before signing in with SSO."; + +describe("LoginPage", () => { + test("shows an invite-only message when SSO returns access_denied", async () => { + const queryClient = createTestQueryClient(); + render( + + + , + ); + + expect(await screen.findByText(inviteOnlyMessage)).toBeTruthy(); + }); + + test("shows an invite-only message for URL-encoded invitation errors", async () => { + const queryClient = createTestQueryClient(); + render( + + + , + ); + + expect(await screen.findByText(inviteOnlyMessage)).toBeTruthy(); + }); +}); diff --git a/app/client/modules/auth/routes/login.tsx b/app/client/modules/auth/routes/login.tsx index c4085632..e9f05d80 100644 --- a/app/client/modules/auth/routes/login.tsx +++ b/app/client/modules/auth/routes/login.tsx @@ -10,9 +10,13 @@ import { Input } from "~/client/components/ui/input"; import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "~/client/components/ui/input-otp"; import { Label } from "~/client/components/ui/label"; import { authClient } from "~/client/lib/auth-client"; +import { decodeLoginError, getLoginErrorDescription } from "~/client/lib/auth-errors"; import { ResetPasswordDialog } from "../components/reset-password-dialog"; import { useNavigate } from "@tanstack/react-router"; import { normalizeUsername } from "~/lib/username"; +import { useMutation, useSuspenseQuery } from "@tanstack/react-query"; +import { getPublicSsoProvidersOptions } from "~/client/api-client/@tanstack/react-query.gen"; +import { cn } from "~/client/lib/utils"; const loginSchema = type({ username: "2<=string<=50", @@ -21,7 +25,11 @@ const loginSchema = type({ type LoginFormValues = typeof loginSchema.inferIn; -export function LoginPage() { +type LoginPageProps = { + error?: string; +}; + +export function LoginPage({ error }: LoginPageProps = {}) { const navigate = useNavigate(); const [showResetDialog, setShowResetDialog] = useState(false); const [isLoggingIn, setIsLoggingIn] = useState(false); @@ -29,6 +37,12 @@ export function LoginPage() { const [totpCode, setTotpCode] = useState(""); const [isVerifying2FA, setIsVerifying2FA] = useState(false); const [trustDevice, setTrustDevice] = useState(false); + const errorCode = decodeLoginError(error); + const errorDescription = getLoginErrorDescription(errorCode); + + const { data: ssoProviders } = useSuspenseQuery({ + ...getPublicSsoProvidersOptions(), + }); const form = useForm({ resolver: arktypeResolver(loginSchema), @@ -115,6 +129,27 @@ export function LoginPage() { form.reset(); }; + const ssoLoginMutation = useMutation({ + mutationFn: async (providerId: string) => { + const callbackPath = "/login"; + const { data, error } = await authClient.signIn.sso({ + providerId: providerId, + callbackURL: callbackPath, + errorCallbackURL: callbackPath, + }); + if (error) throw error; + + return data; + }, + onSuccess: (data) => { + window.location.href = data.url; + }, + onError: (error) => { + console.error(error); + toast.error("SSO Login failed", { description: error.message }); + }, + }); + if (requires2FA) { return ( @@ -186,6 +221,9 @@ export function LoginPage() {
+
+ {errorDescription} +
+ {ssoProviders.providers.length > 0 && ( +
+

Alternative Sign-in

+
+ {ssoProviders.providers.map((provider) => ( + + ))} +
+
+ )} +
); diff --git a/app/client/modules/settings/components/sso/sso-settings-section.tsx b/app/client/modules/settings/components/sso/sso-settings-section.tsx new file mode 100644 index 00000000..b028c481 --- /dev/null +++ b/app/client/modules/settings/components/sso/sso-settings-section.tsx @@ -0,0 +1,366 @@ +import { useMutation, useSuspenseQuery } from "@tanstack/react-query"; +import { useNavigate } from "@tanstack/react-router"; +import { Ban, Trash2 } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import { + deleteSsoInvitationMutation, + deleteSsoProviderMutation, + getSsoSettingsOptions, + updateSsoProviderAutoLinkingMutation, +} from "~/client/api-client/@tanstack/react-query.gen"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "~/client/components/ui/alert-dialog"; +import { Alert, AlertDescription } from "~/client/components/ui/alert"; +import { Button } from "~/client/components/ui/button"; +import { Input } from "~/client/components/ui/input"; +import { Label } from "~/client/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select"; +import { Switch } from "~/client/components/ui/switch"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table"; +import { useOrganizationContext } from "~/client/hooks/use-org-context"; +import { formatDateWithMonth } from "~/client/lib/datetime"; +import { getOrigin } from "~/client/functions/get-origin"; +import { authClient } from "~/client/lib/auth-client"; +import { cn } from "~/client/lib/utils"; + +type InvitationRole = "member" | "admin" | "owner"; + +export function SsoSettingsSection() { + const origin = getOrigin(); + const navigate = useNavigate(); + const { activeOrganization } = useOrganizationContext(); + const [inviteEmail, setInviteEmail] = useState(""); + const [inviteRole, setInviteRole] = useState("member"); + + const { data } = useSuspenseQuery({ + ...getSsoSettingsOptions(), + }); + + const providers = data.providers; + const invitations = data.invitations; + + const updateProviderAutoLinkingMutation = useMutation({ + ...updateSsoProviderAutoLinkingMutation(), + onSuccess: (_, v) => { + toast.success(v.body?.enabled ? "Automatic account linking enabled" : "Automatic account linking disabled"); + }, + onError: (error) => { + toast.error("Failed to update provider", { description: error.message }); + }, + }); + + const deleteProviderMutation = useMutation({ + ...deleteSsoProviderMutation(), + onSuccess: () => { + toast.success("SSO provider deleted"); + }, + onError: (error) => { + toast.error("Failed to delete provider", { description: error.message }); + }, + }); + + const inviteMemberMutation = useMutation({ + mutationFn: async () => { + if (!activeOrganization) { + throw new Error("No active organization found in session"); + } + + const normalizedEmail = inviteEmail.trim().toLowerCase(); + if (!normalizedEmail) { + throw new Error("Email is required"); + } + + const { error } = await authClient.organization.inviteMember({ + email: normalizedEmail, + role: inviteRole, + organizationId: activeOrganization.id, + }); + + if (error) { + throw error; + } + }, + onSuccess: () => { + toast.success("Invitation created"); + setInviteEmail(""); + setInviteRole("member"); + }, + onError: (error) => { + toast.error("Failed to create invitation", { description: error.message }); + }, + }); + + const cancelInvitationMutation = useMutation({ + mutationFn: async (invitationId: string) => { + const { error } = await authClient.organization.cancelInvitation({ invitationId }); + if (error) { + throw error; + } + }, + onSuccess: () => { + toast.success("Invitation cancelled"); + }, + onError: (error) => { + toast.error("Failed to cancel invitation", { description: error.message }); + }, + }); + + const deleteInvitationMutation = useMutation({ + ...deleteSsoInvitationMutation(), + onSuccess: () => { + toast.success("Invitation deleted"); + }, + onError: (error) => { + toast.error("Failed to delete invitation", { description: error.message }); + }, + }); + + return ( +
+
+
+
+

Registered providers

+

Manage identity providers used for organization sign-in.

+
+ + +
+ + + + Only enable automatic account linking for identity providers you trust. You can change this per provider at + any time. + + + +
+ + + + Provider ID + Domain + Issuer + Type + Auto-link existing account + Callback URL + Actions + + + + {providers.map((provider) => ( + + {provider.providerId} + {provider.domain} + {provider.issuer} + +
+ {provider.type} +
+
+ +
+ { + updateProviderAutoLinkingMutation.mutate({ + path: { providerId: provider.providerId }, + body: { enabled }, + }); + }} + /> + + {provider.autoLinkMatchingEmails ? "On" : "Off"} + +
+
+ + e.currentTarget.select()} + /> + + + + + + + + + Delete SSO provider + + Are you sure you want to delete the SSO provider {provider.providerId}? + This action cannot be undone. + + + + Cancel + deleteProviderMutation.mutate({ path: { providerId: provider.providerId } })} + > + Delete + + + + + +
+ ))} + 0 })}> + + No providers registered yet. + + +
+
+
+
+ +
+
+

Invite-only access

+

+ Users must be invited or already have an account before they can sign in using SSO. +

+
+ +
+
+ + setInviteEmail(event.target.value)} + placeholder="teammate@example.com" + disabled={!activeOrganization || inviteMemberMutation.isPending} + /> +
+ +
+ + +
+ +
+ +
+
+ +
+ + + + Email + Role + Status + Expires + Actions + + + + {invitations.map((invitation) => ( + + {invitation.email} + {invitation.role} + + + {invitation.status} + + + {formatDateWithMonth(invitation.expiresAt)} + + + + + + ))} + 0 })}> + + No invitations yet. + + + +
+
+
+
+ ); +} diff --git a/app/client/modules/settings/components/user-management.tsx b/app/client/modules/settings/components/user-management.tsx index ccb96a3f..e1c24730 100644 --- a/app/client/modules/settings/components/user-management.tsx +++ b/app/client/modules/settings/components/user-management.tsx @@ -1,5 +1,5 @@ -import { useMutation, useQuery } from "@tanstack/react-query"; -import { Shield, ShieldAlert, UserMinus, UserCheck, Trash2, Search, AlertTriangle } from "lucide-react"; +import { useMutation, useQuery, useSuspenseQuery } from "@tanstack/react-query"; +import { Shield, ShieldAlert, UserCheck, Trash2, Search, AlertTriangle, Ban, KeyRound } from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; import { authClient } from "~/client/lib/auth-client"; @@ -17,29 +17,28 @@ import { DialogTitle, } from "~/client/components/ui/dialog"; import { CreateUserDialog } from "./create-user-dialog"; -import { getUserDeletionImpactOptions } from "~/client/api-client/@tanstack/react-query.gen"; - -export function UserManagement() { - const { data: session } = authClient.useSession(); - const currentUser = session?.user; +import { + getAdminUsersOptions, + getUserDeletionImpactOptions, + deleteUserAccountMutation, +} from "~/client/api-client/@tanstack/react-query.gen"; +export function UserManagement({ currentUser }: { currentUser: { id: string } | undefined | null }) { const [search, setSearch] = useState(""); const [userToDelete, setUserToDelete] = useState(null); const [userToBan, setUserToBan] = useState<{ id: string; name: string; isBanned: boolean } | null>(null); + const [userToManageAccounts, setUserToManageAccounts] = useState<{ + id: string; + name: string; + accounts: { id: string; providerId: string }[]; + }>(); const { data: deletionImpact, isLoading: isLoadingImpact } = useQuery({ ...getUserDeletionImpactOptions({ path: { userId: userToDelete ?? "" } }), enabled: Boolean(userToDelete), }); - const { data, isLoading, refetch } = useQuery({ - queryKey: ["admin-users"], - queryFn: async () => { - const { data, error } = await authClient.admin.listUsers({ query: { limit: 100 } }); - if (error) throw error; - return data; - }, - }); + const { data } = useSuspenseQuery({ ...getAdminUsersOptions() }); const setRoleMutation = useMutation({ mutationFn: async ({ userId, role }: { userId: string; role: "user" | "admin" }) => { @@ -48,10 +47,9 @@ export function UserManagement() { }, onSuccess: () => { toast.success("User role updated successfully"); - void refetch(); }, - onError: (err: any) => { - toast.error("Failed to update role", { description: err.message }); + onError: (error) => { + toast.error("Failed to update role", { description: error.message }); }, }); @@ -62,36 +60,53 @@ export function UserManagement() { }, onSuccess: () => { toast.success("User ban status updated successfully"); - void refetch(); }, onMutate: () => { setUserToBan(null); }, - onError: (err: any) => { - toast.error("Failed to update ban status", { description: err.message }); + onError: (error) => { + toast.error("Failed to update ban status", { description: error.message }); }, }); - const filteredUsers = data?.users.filter( - (user) => - user.name.toLowerCase().includes(search.toLowerCase()) || - user.email.toLowerCase().includes(search.toLowerCase()) || - (user as any).username?.toLowerCase().includes(search.toLowerCase()), - ); - - const handleDeleteUser = async () => { - if (!userToDelete) return; - - try { - const { error } = await authClient.admin.removeUser({ userId: userToDelete }); + const deleteUser = useMutation({ + mutationFn: async (userId: string) => { + const { error } = await authClient.admin.removeUser({ userId }); if (error) throw error; + }, + onSuccess: () => { toast.success("User deleted successfully"); setUserToDelete(null); - void refetch(); - } catch (err: any) { - toast.error("Failed to delete user", { description: err.message }); - } - }; + }, + onError: (error) => { + toast.error("Failed to delete user", { description: error.message }); + }, + }); + + const deleteAccount = useMutation({ + ...deleteUserAccountMutation(), + onSuccess: (_data, variables) => { + toast.success("Account removed successfully"); + setUserToManageAccounts((prev) => { + if (!prev) return prev; + return { + ...prev, + accounts: prev.accounts.filter((a) => a.id !== variables.path.accountId), + }; + }); + }, + onError: (error) => { + toast.error("Failed to remove account", { description: error.message }); + }, + }); + + const normalizedSearch = search.toLowerCase(); + const filteredUsers = data.users.filter((user) => { + const name = user.name?.toLowerCase() ?? ""; + const email = user.email.toLowerCase(); + + return name.includes(normalizedSearch) || email.includes(normalizedSearch); + }); return (
@@ -105,7 +120,7 @@ export function UserManagement() { onChange={(e) => setSearch(e.target.value)} />
- void refetch()} /> +
@@ -119,21 +134,16 @@ export function UserManagement() { - - - Loading users... - - - 0) })}> + 0 })}> No users found. - {filteredUsers?.map((user) => ( + {filteredUsers.map((user) => (
- {user.name} + {user.name ?? user.email} {user.email}
@@ -149,12 +159,12 @@ export function UserManagement() { -
+
- + +
@@ -242,7 +273,11 @@ export function UserManagement() { - @@ -267,20 +302,60 @@ export function UserManagement() { + + !open && setUserToManageAccounts(undefined)}> + + + Manage Accounts + Linked authentication accounts for {userToManageAccounts?.name}. + + +
+

+ No accounts linked. +

+ {userToManageAccounts?.accounts.map((account) => ( +
+ {account.providerId} + +
+ ))} +
+ + + +
+
); } diff --git a/app/client/modules/settings/routes/create-sso-provider.tsx b/app/client/modules/settings/routes/create-sso-provider.tsx new file mode 100644 index 00000000..d200da4c --- /dev/null +++ b/app/client/modules/settings/routes/create-sso-provider.tsx @@ -0,0 +1,258 @@ +import { arktypeResolver } from "@hookform/resolvers/arktype"; +import { useMutation } from "@tanstack/react-query"; +import { useNavigate } from "@tanstack/react-router"; +import { type } from "arktype"; +import { ShieldCheck, Plus } from "lucide-react"; +import { useId } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { updateSsoProviderAutoLinkingMutation } from "~/client/api-client/@tanstack/react-query.gen"; +import { Button } from "~/client/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "~/client/components/ui/form"; +import { Input } from "~/client/components/ui/input"; +import { Switch } from "~/client/components/ui/switch"; +import { authClient } from "~/client/lib/auth-client"; +import { parseError } from "~/client/lib/errors"; +import { useOrganizationContext } from "~/client/hooks/use-org-context"; + +const ssoProviderSchema = type({ + providerId: "string>=1", + issuer: "string>=1", + domain: "string>=1", + clientId: "string>=1", + clientSecret: "string>=1", + discoveryEndpoint: "string>=1", + linkMatchingEmails: "boolean", +}); + +type ProviderForm = typeof ssoProviderSchema.infer; + +export function CreateSsoProviderPage() { + const navigate = useNavigate(); + const formId = useId(); + const { activeOrganization } = useOrganizationContext(); + + const form = useForm({ + resolver: arktypeResolver(ssoProviderSchema), + defaultValues: { + providerId: "", + issuer: "", + domain: "", + clientId: "", + clientSecret: "", + discoveryEndpoint: "", + linkMatchingEmails: false, + }, + }); + + const updateProviderAutoLinking = useMutation({ + ...updateSsoProviderAutoLinkingMutation(), + }); + + const registerProvider = useMutation({ + mutationFn: async (formData: ProviderForm) => { + const { error } = await authClient.sso.register({ + providerId: formData.providerId, + issuer: formData.issuer, + domain: formData.domain, + organizationId: activeOrganization.id, + oidcConfig: { + clientId: formData.clientId, + clientSecret: formData.clientSecret, + discoveryEndpoint: formData.discoveryEndpoint, + scopes: ["openid", "email", "profile"], + }, + }); + + if (error) throw error; + + await updateProviderAutoLinking + .mutateAsync({ + path: { providerId: formData.providerId }, + body: { enabled: formData.linkMatchingEmails }, + }) + .catch((updateError) => { + toast.warning("Auto-link setting could not be saved", { + description: parseError(updateError)?.message, + }); + }); + }, + onSuccess: () => { + toast.success("SSO provider registered successfully"); + void navigate({ to: "/settings", search: { tab: "users" } }); + }, + onError: (error) => { + toast.error("Failed to register provider", { description: error.message }); + }, + }); + + return ( +
+ + +
+
+ +
+ Register SSO Provider +
+
+ +
+ registerProvider.mutate(values))} + className="space-y-4" + > +
+ ( + + Provider ID + + + + Unique identifier used in callback URLs. + + + )} + /> + + ( + + Organization Domain + + + + Used to discover providers during login. + + + )} + /> + + ( + + Issuer URL + + + + + + )} + /> + + ( + + Discovery Endpoint + + + + + + )} + /> + + ( + + Client ID + + + + + + )} + /> + + ( + + Client Secret + + + + + + )} + /> +
+ + ( + +
+
+ Link matching emails to existing accounts + + If enabled, users who sign in with this provider will automatically access their existing + account when the email address matches. + +
+ + + +
+ +
+ )} + /> + + + +
+ + +
+
+
+
+ ); +} diff --git a/app/client/modules/settings/routes/settings.tsx b/app/client/modules/settings/routes/settings.tsx index d46d2360..6b74edcd 100644 --- a/app/client/modules/settings/routes/settings.tsx +++ b/app/client/modules/settings/routes/settings.tsx @@ -27,6 +27,7 @@ import { type AppContext } from "~/context"; import { TwoFactorSection } from "../components/two-factor-section"; import { UserManagement } from "../components/user-management"; import { useNavigate, useSearch } from "@tanstack/react-router"; +import { SsoSettingsSection } from "../components/sso/sso-settings-section"; type Props = { appContext: AppContext; @@ -165,7 +166,7 @@ export function SettingsPage({ appContext }: Props) { Account - {isAdmin && Users} + {isAdmin && Users & Authentication} {isAdmin && System} @@ -307,7 +308,7 @@ export function SettingsPage({ appContext }: Props) { {isAdmin && ( - +
@@ -316,7 +317,20 @@ export function SettingsPage({ appContext }: Props) { Manage users, roles and permissions
- + +
+ + +
+ + + Single Sign-On + + Configure OIDC provider settings +
+ + +
)} diff --git a/app/context.ts b/app/context.ts index fb654db3..0ae36afd 100644 --- a/app/context.ts +++ b/app/context.ts @@ -2,6 +2,7 @@ type User = { id: string; email: string; username: string; + name: string; hasDownloadedResticPassword: boolean; twoFactorEnabled?: boolean | null; role?: string | null | undefined; diff --git a/app/drizzle/20260227200731_sad_luke_cage/migration.sql b/app/drizzle/20260227200731_sad_luke_cage/migration.sql new file mode 100644 index 00000000..8a0600cb --- /dev/null +++ b/app/drizzle/20260227200731_sad_luke_cage/migration.sql @@ -0,0 +1,15 @@ +CREATE TABLE `sso_provider` ( + `id` text PRIMARY KEY, + `provider_id` text NOT NULL UNIQUE, + `organization_id` text NOT NULL, + `user_id` text, + `issuer` text NOT NULL, + `domain` text NOT NULL, + `auto_link_matching_emails` integer DEFAULT false NOT NULL, + `oidc_config` text, + `saml_config` text, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + CONSTRAINT `fk_sso_provider_organization_id_organization_id_fk` FOREIGN KEY (`organization_id`) REFERENCES `organization`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_sso_provider_user_id_users_table_id_fk` FOREIGN KEY (`user_id`) REFERENCES `users_table`(`id`) ON DELETE SET NULL +); diff --git a/app/drizzle/20260227200731_sad_luke_cage/snapshot.json b/app/drizzle/20260227200731_sad_luke_cage/snapshot.json new file mode 100644 index 00000000..fda9380d --- /dev/null +++ b/app/drizzle/20260227200731_sad_luke_cage/snapshot.json @@ -0,0 +1,2185 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "60a53b0c-395a-4960-9b48-1740388bd2dc", + "prevIds": ["3a308c54-d950-464f-9490-fee06985fbeb"], + "ddl": [ + { + "name": "account", + "entityType": "tables" + }, + { + "name": "app_metadata", + "entityType": "tables" + }, + { + "name": "backup_schedule_mirrors_table", + "entityType": "tables" + }, + { + "name": "backup_schedule_notifications_table", + "entityType": "tables" + }, + { + "name": "backup_schedules_table", + "entityType": "tables" + }, + { + "name": "invitation", + "entityType": "tables" + }, + { + "name": "member", + "entityType": "tables" + }, + { + "name": "notification_destinations_table", + "entityType": "tables" + }, + { + "name": "organization", + "entityType": "tables" + }, + { + "name": "repositories_table", + "entityType": "tables" + }, + { + "name": "sessions_table", + "entityType": "tables" + }, + { + "name": "sso_provider", + "entityType": "tables" + }, + { + "name": "two_factor", + "entityType": "tables" + }, + { + "name": "users_table", + "entityType": "tables" + }, + { + "name": "verification", + "entityType": "tables" + }, + { + "name": "volumes_table", + "entityType": "tables" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "account_id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "provider_id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token_expires_at", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token_expires_at", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "scope", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "password", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "key", + "entityType": "columns", + "table": "app_metadata" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "value", + "entityType": "columns", + "table": "app_metadata" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "app_metadata" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "app_metadata" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "backup_schedule_mirrors_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "schedule_id", + "entityType": "columns", + "table": "backup_schedule_mirrors_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "repository_id", + "entityType": "columns", + "table": "backup_schedule_mirrors_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "true", + "generated": null, + "name": "enabled", + "entityType": "columns", + "table": "backup_schedule_mirrors_table" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_copy_at", + "entityType": "columns", + "table": "backup_schedule_mirrors_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_copy_status", + "entityType": "columns", + "table": "backup_schedule_mirrors_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_copy_error", + "entityType": "columns", + "table": "backup_schedule_mirrors_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "backup_schedule_mirrors_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "schedule_id", + "entityType": "columns", + "table": "backup_schedule_notifications_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "destination_id", + "entityType": "columns", + "table": "backup_schedule_notifications_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "notify_on_start", + "entityType": "columns", + "table": "backup_schedule_notifications_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "notify_on_success", + "entityType": "columns", + "table": "backup_schedule_notifications_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "true", + "generated": null, + "name": "notify_on_warning", + "entityType": "columns", + "table": "backup_schedule_notifications_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "true", + "generated": null, + "name": "notify_on_failure", + "entityType": "columns", + "table": "backup_schedule_notifications_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "backup_schedule_notifications_table" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "short_id", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "volume_id", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "repository_id", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "true", + "generated": null, + "name": "enabled", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "cron_expression", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "retention_policy", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": "'[]'", + "generated": null, + "name": "exclude_patterns", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": "'[]'", + "generated": null, + "name": "exclude_if_present", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": "'[]'", + "generated": null, + "name": "include_patterns", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_backup_at", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_backup_status", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_backup_error", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "next_backup_at", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "one_file_system", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "sort_order", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "organization_id", + "entityType": "columns", + "table": "backup_schedules_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "invitation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "organization_id", + "entityType": "columns", + "table": "invitation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "invitation" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "role", + "entityType": "columns", + "table": "invitation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'pending'", + "generated": null, + "name": "status", + "entityType": "columns", + "table": "invitation" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "expires_at", + "entityType": "columns", + "table": "invitation" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "invitation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "inviter_id", + "entityType": "columns", + "table": "invitation" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "member" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "organization_id", + "entityType": "columns", + "table": "member" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "member" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'member'", + "generated": null, + "name": "role", + "entityType": "columns", + "table": "member" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "member" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "notification_destinations_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "notification_destinations_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "true", + "generated": null, + "name": "enabled", + "entityType": "columns", + "table": "notification_destinations_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "notification_destinations_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "config", + "entityType": "columns", + "table": "notification_destinations_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "notification_destinations_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "notification_destinations_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "organization_id", + "entityType": "columns", + "table": "notification_destinations_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "organization" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "organization" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "organization" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "logo", + "entityType": "columns", + "table": "organization" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "organization" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "metadata", + "entityType": "columns", + "table": "organization" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "short_id", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "config", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": "'auto'", + "generated": null, + "name": "compression_mode", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": "'unknown'", + "generated": null, + "name": "status", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_checked", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_error", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "doctor_result", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "upload_limit_enabled", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "real", + "notNull": true, + "autoincrement": false, + "default": "1", + "generated": null, + "name": "upload_limit_value", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'Mbps'", + "generated": null, + "name": "upload_limit_unit", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "download_limit_enabled", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "real", + "notNull": true, + "autoincrement": false, + "default": "1", + "generated": null, + "name": "download_limit_value", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'Mbps'", + "generated": null, + "name": "download_limit_unit", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "organization_id", + "entityType": "columns", + "table": "repositories_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "sessions_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "sessions_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token", + "entityType": "columns", + "table": "sessions_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "expires_at", + "entityType": "columns", + "table": "sessions_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "sessions_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "sessions_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ip_address", + "entityType": "columns", + "table": "sessions_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "user_agent", + "entityType": "columns", + "table": "sessions_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "impersonated_by", + "entityType": "columns", + "table": "sessions_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_organization_id", + "entityType": "columns", + "table": "sessions_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "sso_provider" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "provider_id", + "entityType": "columns", + "table": "sso_provider" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "organization_id", + "entityType": "columns", + "table": "sso_provider" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "sso_provider" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "issuer", + "entityType": "columns", + "table": "sso_provider" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "domain", + "entityType": "columns", + "table": "sso_provider" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "auto_link_matching_emails", + "entityType": "columns", + "table": "sso_provider" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "oidc_config", + "entityType": "columns", + "table": "sso_provider" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "saml_config", + "entityType": "columns", + "table": "sso_provider" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "sso_provider" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "sso_provider" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "two_factor" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret", + "entityType": "columns", + "table": "two_factor" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "backup_codes", + "entityType": "columns", + "table": "two_factor" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "two_factor" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "username", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "password_hash", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "has_downloaded_restic_password", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "email_verified", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "image", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "display_username", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "two_factor_enabled", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'user'", + "generated": null, + "name": "role", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "banned", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ban_reason", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ban_expires", + "entityType": "columns", + "table": "users_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "verification" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "identifier", + "entityType": "columns", + "table": "verification" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "value", + "entityType": "columns", + "table": "verification" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "expires_at", + "entityType": "columns", + "table": "verification" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "verification" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "verification" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "volumes_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "short_id", + "entityType": "columns", + "table": "volumes_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "volumes_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "volumes_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'unmounted'", + "generated": null, + "name": "status", + "entityType": "columns", + "table": "volumes_table" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_error", + "entityType": "columns", + "table": "volumes_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "last_health_check", + "entityType": "columns", + "table": "volumes_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "volumes_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "volumes_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "config", + "entityType": "columns", + "table": "volumes_table" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "true", + "generated": null, + "name": "auto_remount", + "entityType": "columns", + "table": "volumes_table" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "organization_id", + "entityType": "columns", + "table": "volumes_table" + }, + { + "columns": ["user_id"], + "tableTo": "users_table", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "account_user_id_users_table_id_fk", + "entityType": "fks", + "table": "account" + }, + { + "columns": ["schedule_id"], + "tableTo": "backup_schedules_table", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk", + "entityType": "fks", + "table": "backup_schedule_mirrors_table" + }, + { + "columns": ["repository_id"], + "tableTo": "repositories_table", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk", + "entityType": "fks", + "table": "backup_schedule_mirrors_table" + }, + { + "columns": ["schedule_id"], + "tableTo": "backup_schedules_table", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk", + "entityType": "fks", + "table": "backup_schedule_notifications_table" + }, + { + "columns": ["destination_id"], + "tableTo": "notification_destinations_table", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk", + "entityType": "fks", + "table": "backup_schedule_notifications_table" + }, + { + "columns": ["volume_id"], + "tableTo": "volumes_table", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "backup_schedules_table_volume_id_volumes_table_id_fk", + "entityType": "fks", + "table": "backup_schedules_table" + }, + { + "columns": ["repository_id"], + "tableTo": "repositories_table", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "backup_schedules_table_repository_id_repositories_table_id_fk", + "entityType": "fks", + "table": "backup_schedules_table" + }, + { + "columns": ["organization_id"], + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "backup_schedules_table_organization_id_organization_id_fk", + "entityType": "fks", + "table": "backup_schedules_table" + }, + { + "columns": ["organization_id"], + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "invitation_organization_id_organization_id_fk", + "entityType": "fks", + "table": "invitation" + }, + { + "columns": ["inviter_id"], + "tableTo": "users_table", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "invitation_inviter_id_users_table_id_fk", + "entityType": "fks", + "table": "invitation" + }, + { + "columns": ["organization_id"], + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "member_organization_id_organization_id_fk", + "entityType": "fks", + "table": "member" + }, + { + "columns": ["user_id"], + "tableTo": "users_table", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "member_user_id_users_table_id_fk", + "entityType": "fks", + "table": "member" + }, + { + "columns": ["organization_id"], + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "notification_destinations_table_organization_id_organization_id_fk", + "entityType": "fks", + "table": "notification_destinations_table" + }, + { + "columns": ["organization_id"], + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "repositories_table_organization_id_organization_id_fk", + "entityType": "fks", + "table": "repositories_table" + }, + { + "columns": ["user_id"], + "tableTo": "users_table", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "sessions_table_user_id_users_table_id_fk", + "entityType": "fks", + "table": "sessions_table" + }, + { + "columns": ["organization_id"], + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_sso_provider_organization_id_organization_id_fk", + "entityType": "fks", + "table": "sso_provider" + }, + { + "columns": ["user_id"], + "tableTo": "users_table", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "nameExplicit": false, + "name": "fk_sso_provider_user_id_users_table_id_fk", + "entityType": "fks", + "table": "sso_provider" + }, + { + "columns": ["user_id"], + "tableTo": "users_table", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "two_factor_user_id_users_table_id_fk", + "entityType": "fks", + "table": "two_factor" + }, + { + "columns": ["organization_id"], + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "volumes_table_organization_id_organization_id_fk", + "entityType": "fks", + "table": "volumes_table" + }, + { + "columns": ["schedule_id", "destination_id"], + "nameExplicit": false, + "name": "backup_schedule_notifications_table_schedule_id_destination_id_pk", + "entityType": "pks", + "table": "backup_schedule_notifications_table" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "account_pk", + "table": "account", + "entityType": "pks" + }, + { + "columns": ["key"], + "nameExplicit": false, + "name": "app_metadata_pk", + "table": "app_metadata", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "backup_schedule_mirrors_table_pk", + "table": "backup_schedule_mirrors_table", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "backup_schedules_table_pk", + "table": "backup_schedules_table", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "invitation_pk", + "table": "invitation", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "member_pk", + "table": "member", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "notification_destinations_table_pk", + "table": "notification_destinations_table", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "organization_pk", + "table": "organization", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "repositories_table_pk", + "table": "repositories_table", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "sessions_table_pk", + "table": "sessions_table", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "sso_provider_pk", + "table": "sso_provider", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "two_factor_pk", + "table": "two_factor", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "users_table_pk", + "table": "users_table", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "verification_pk", + "table": "verification", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "volumes_table_pk", + "table": "volumes_table", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "account_userId_idx", + "entityType": "indexes", + "table": "account" + }, + { + "columns": [ + { + "value": "organization_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "invitation_organizationId_idx", + "entityType": "indexes", + "table": "invitation" + }, + { + "columns": [ + { + "value": "email", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "invitation_email_idx", + "entityType": "indexes", + "table": "invitation" + }, + { + "columns": [ + { + "value": "organization_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "member_organizationId_idx", + "entityType": "indexes", + "table": "member" + }, + { + "columns": [ + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "member_userId_idx", + "entityType": "indexes", + "table": "member" + }, + { + "columns": [ + { + "value": "organization_id", + "isExpression": false + }, + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "member_org_user_uidx", + "entityType": "indexes", + "table": "member" + }, + { + "columns": [ + { + "value": "slug", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "organization_slug_uidx", + "entityType": "indexes", + "table": "organization" + }, + { + "columns": [ + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "sessionsTable_userId_idx", + "entityType": "indexes", + "table": "sessions_table" + }, + { + "columns": [ + { + "value": "secret", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "twoFactor_secret_idx", + "entityType": "indexes", + "table": "two_factor" + }, + { + "columns": [ + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "twoFactor_userId_idx", + "entityType": "indexes", + "table": "two_factor" + }, + { + "columns": [ + { + "value": "identifier", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "verification_identifier_idx", + "entityType": "indexes", + "table": "verification" + }, + { + "columns": ["schedule_id", "repository_id"], + "nameExplicit": false, + "name": "backup_schedule_mirrors_table_schedule_id_repository_id_unique", + "entityType": "uniques", + "table": "backup_schedule_mirrors_table" + }, + { + "columns": ["name", "organization_id"], + "nameExplicit": false, + "name": "volumes_table_name_organization_id_unique", + "entityType": "uniques", + "table": "volumes_table" + }, + { + "columns": ["short_id"], + "nameExplicit": false, + "name": "backup_schedules_table_short_id_unique", + "entityType": "uniques", + "table": "backup_schedules_table" + }, + { + "columns": ["short_id"], + "nameExplicit": false, + "name": "repositories_table_short_id_unique", + "entityType": "uniques", + "table": "repositories_table" + }, + { + "columns": ["token"], + "nameExplicit": false, + "name": "sessions_table_token_unique", + "entityType": "uniques", + "table": "sessions_table" + }, + { + "columns": ["provider_id"], + "nameExplicit": false, + "name": "sso_provider_provider_id_unique", + "entityType": "uniques", + "table": "sso_provider" + }, + { + "columns": ["username"], + "nameExplicit": false, + "name": "users_table_username_unique", + "entityType": "uniques", + "table": "users_table" + }, + { + "columns": ["email"], + "nameExplicit": false, + "name": "users_table_email_unique", + "entityType": "uniques", + "table": "users_table" + }, + { + "columns": ["short_id"], + "nameExplicit": false, + "name": "volumes_table_short_id_unique", + "entityType": "uniques", + "table": "volumes_table" + } + ], + "renames": [] +} diff --git a/app/middleware/auth.ts b/app/middleware/auth.ts index 6d987d9a..94e49201 100644 --- a/app/middleware/auth.ts +++ b/app/middleware/auth.ts @@ -4,13 +4,20 @@ import { auth } from "~/server/lib/auth"; import { getRequestHeaders } from "@tanstack/react-start/server"; import { authService } from "~/server/modules/auth/auth.service"; +function isAuthRoute(pathname: string): boolean { + if (pathname === "/onboarding") return true; + if (pathname === "/login") return true; + if (pathname.match(/^\/login\/[^/]+$/)) return true; + if (pathname.match(/^\/login\/[^/]+\/error$/)) return true; + return false; +} + export const authMiddleware = createMiddleware().server(async ({ next, request }) => { const headers = getRequestHeaders(); const session = await auth.api.getSession({ headers }); + const pathname = new URL(request.url).pathname; - const isAuthRoute = ["/login", "/onboarding"].includes(new URL(request.url).pathname); - - if (!session?.user?.id && !isAuthRoute) { + if (!session?.user?.id && !isAuthRoute(pathname)) { const hasUsers = await authService.hasUsers(); if (!hasUsers) { throw redirect({ to: "/onboarding" }); @@ -20,8 +27,12 @@ export const authMiddleware = createMiddleware().server(async ({ next, request } } if (session?.user?.id) { - if (isAuthRoute) { - throw redirect({ to: "/" }); + if (!session.user.hasDownloadedResticPassword && pathname !== "/download-recovery-key") { + throw redirect({ to: "/download-recovery-key" }); + } + + if (isAuthRoute(pathname)) { + throw redirect({ to: "/volumes" }); } } diff --git a/app/routeTree.gen.ts b/app/routeTree.gen.ts index c9067284..696df13b 100644 --- a/app/routeTree.gen.ts +++ b/app/routeTree.gen.ts @@ -10,6 +10,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as dashboardRouteRouteImport } from './routes/(dashboard)/route' +import { Route as authRouteRouteImport } from './routes/(auth)/route' import { Route as IndexRouteImport } from './routes/index' import { Route as ApiSplatRouteImport } from './routes/api.$' import { Route as authOnboardingRouteImport } from './routes/(auth)/onboarding' @@ -26,8 +27,10 @@ import { Route as dashboardRepositoriesCreateRouteImport } from './routes/(dashb import { Route as dashboardNotificationsCreateRouteImport } from './routes/(dashboard)/notifications/create' import { Route as dashboardNotificationsNotificationIdRouteImport } from './routes/(dashboard)/notifications/$notificationId' import { Route as dashboardBackupsCreateRouteImport } from './routes/(dashboard)/backups/create' +import { Route as authLoginErrorRouteImport } from './routes/(auth)/login.error' import { Route as dashboardRepositoriesRepositoryIdIndexRouteImport } from './routes/(dashboard)/repositories/$repositoryId/index' import { Route as dashboardBackupsBackupIdIndexRouteImport } from './routes/(dashboard)/backups/$backupId/index' +import { Route as dashboardSettingsSsoNewRouteImport } from './routes/(dashboard)/settings/sso/new' import { Route as dashboardRepositoriesRepositoryIdEditRouteImport } from './routes/(dashboard)/repositories/$repositoryId/edit' import { Route as dashboardRepositoriesRepositoryIdSnapshotIdIndexRouteImport } from './routes/(dashboard)/repositories/$repositoryId/$snapshotId/index' import { Route as dashboardRepositoriesRepositoryIdSnapshotIdRestoreRouteImport } from './routes/(dashboard)/repositories/$repositoryId/$snapshotId/restore' @@ -37,6 +40,10 @@ const dashboardRouteRoute = dashboardRouteRouteImport.update({ id: '/(dashboard)', getParentRoute: () => rootRouteImport, } as any) +const authRouteRoute = authRouteRouteImport.update({ + id: '/(auth)', + getParentRoute: () => rootRouteImport, +} as any) const IndexRoute = IndexRouteImport.update({ id: '/', path: '/', @@ -48,19 +55,19 @@ const ApiSplatRoute = ApiSplatRouteImport.update({ getParentRoute: () => rootRouteImport, } as any) const authOnboardingRoute = authOnboardingRouteImport.update({ - id: '/(auth)/onboarding', + id: '/onboarding', path: '/onboarding', - getParentRoute: () => rootRouteImport, + getParentRoute: () => authRouteRoute, } as any) const authLoginRoute = authLoginRouteImport.update({ - id: '/(auth)/login', + id: '/login', path: '/login', - getParentRoute: () => rootRouteImport, + getParentRoute: () => authRouteRoute, } as any) const authDownloadRecoveryKeyRoute = authDownloadRecoveryKeyRouteImport.update({ - id: '/(auth)/download-recovery-key', + id: '/download-recovery-key', path: '/download-recovery-key', - getParentRoute: () => rootRouteImport, + getParentRoute: () => authRouteRoute, } as any) const dashboardVolumesIndexRoute = dashboardVolumesIndexRouteImport.update({ id: '/volumes/', @@ -123,6 +130,11 @@ const dashboardBackupsCreateRoute = dashboardBackupsCreateRouteImport.update({ path: '/backups/create', getParentRoute: () => dashboardRouteRoute, } as any) +const authLoginErrorRoute = authLoginErrorRouteImport.update({ + id: '/error', + path: '/error', + getParentRoute: () => authLoginRoute, +} as any) const dashboardRepositoriesRepositoryIdIndexRoute = dashboardRepositoriesRepositoryIdIndexRouteImport.update({ id: '/repositories/$repositoryId/', @@ -135,6 +147,11 @@ const dashboardBackupsBackupIdIndexRoute = path: '/backups/$backupId/', getParentRoute: () => dashboardRouteRoute, } as any) +const dashboardSettingsSsoNewRoute = dashboardSettingsSsoNewRouteImport.update({ + id: '/settings/sso/new', + path: '/settings/sso/new', + getParentRoute: () => dashboardRouteRoute, +} as any) const dashboardRepositoriesRepositoryIdEditRoute = dashboardRepositoriesRepositoryIdEditRouteImport.update({ id: '/repositories/$repositoryId/edit', @@ -163,9 +180,10 @@ const dashboardBackupsBackupIdSnapshotIdRestoreRoute = export interface FileRoutesByFullPath { '/': typeof IndexRoute '/download-recovery-key': typeof authDownloadRecoveryKeyRoute - '/login': typeof authLoginRoute + '/login': typeof authLoginRouteWithChildren '/onboarding': typeof authOnboardingRoute '/api/$': typeof ApiSplatRoute + '/login/error': typeof authLoginErrorRoute '/backups/create': typeof dashboardBackupsCreateRoute '/notifications/$notificationId': typeof dashboardNotificationsNotificationIdRoute '/notifications/create': typeof dashboardNotificationsCreateRoute @@ -178,6 +196,7 @@ export interface FileRoutesByFullPath { '/settings/': typeof dashboardSettingsIndexRoute '/volumes/': typeof dashboardVolumesIndexRoute '/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute + '/settings/sso/new': typeof dashboardSettingsSsoNewRoute '/backups/$backupId/': typeof dashboardBackupsBackupIdIndexRoute '/repositories/$repositoryId/': typeof dashboardRepositoriesRepositoryIdIndexRoute '/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute @@ -187,9 +206,10 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { '/': typeof IndexRoute '/download-recovery-key': typeof authDownloadRecoveryKeyRoute - '/login': typeof authLoginRoute + '/login': typeof authLoginRouteWithChildren '/onboarding': typeof authOnboardingRoute '/api/$': typeof ApiSplatRoute + '/login/error': typeof authLoginErrorRoute '/backups/create': typeof dashboardBackupsCreateRoute '/notifications/$notificationId': typeof dashboardNotificationsNotificationIdRoute '/notifications/create': typeof dashboardNotificationsCreateRoute @@ -202,6 +222,7 @@ export interface FileRoutesByTo { '/settings': typeof dashboardSettingsIndexRoute '/volumes': typeof dashboardVolumesIndexRoute '/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute + '/settings/sso/new': typeof dashboardSettingsSsoNewRoute '/backups/$backupId': typeof dashboardBackupsBackupIdIndexRoute '/repositories/$repositoryId': typeof dashboardRepositoriesRepositoryIdIndexRoute '/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute @@ -211,11 +232,13 @@ export interface FileRoutesByTo { export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute + '/(auth)': typeof authRouteRouteWithChildren '/(dashboard)': typeof dashboardRouteRouteWithChildren '/(auth)/download-recovery-key': typeof authDownloadRecoveryKeyRoute - '/(auth)/login': typeof authLoginRoute + '/(auth)/login': typeof authLoginRouteWithChildren '/(auth)/onboarding': typeof authOnboardingRoute '/api/$': typeof ApiSplatRoute + '/(auth)/login/error': typeof authLoginErrorRoute '/(dashboard)/backups/create': typeof dashboardBackupsCreateRoute '/(dashboard)/notifications/$notificationId': typeof dashboardNotificationsNotificationIdRoute '/(dashboard)/notifications/create': typeof dashboardNotificationsCreateRoute @@ -228,6 +251,7 @@ export interface FileRoutesById { '/(dashboard)/settings/': typeof dashboardSettingsIndexRoute '/(dashboard)/volumes/': typeof dashboardVolumesIndexRoute '/(dashboard)/repositories/$repositoryId/edit': typeof dashboardRepositoriesRepositoryIdEditRoute + '/(dashboard)/settings/sso/new': typeof dashboardSettingsSsoNewRoute '/(dashboard)/backups/$backupId/': typeof dashboardBackupsBackupIdIndexRoute '/(dashboard)/repositories/$repositoryId/': typeof dashboardRepositoriesRepositoryIdIndexRoute '/(dashboard)/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute @@ -242,6 +266,7 @@ export interface FileRouteTypes { | '/login' | '/onboarding' | '/api/$' + | '/login/error' | '/backups/create' | '/notifications/$notificationId' | '/notifications/create' @@ -254,6 +279,7 @@ export interface FileRouteTypes { | '/settings/' | '/volumes/' | '/repositories/$repositoryId/edit' + | '/settings/sso/new' | '/backups/$backupId/' | '/repositories/$repositoryId/' | '/backups/$backupId/$snapshotId/restore' @@ -266,6 +292,7 @@ export interface FileRouteTypes { | '/login' | '/onboarding' | '/api/$' + | '/login/error' | '/backups/create' | '/notifications/$notificationId' | '/notifications/create' @@ -278,6 +305,7 @@ export interface FileRouteTypes { | '/settings' | '/volumes' | '/repositories/$repositoryId/edit' + | '/settings/sso/new' | '/backups/$backupId' | '/repositories/$repositoryId' | '/backups/$backupId/$snapshotId/restore' @@ -286,11 +314,13 @@ export interface FileRouteTypes { id: | '__root__' | '/' + | '/(auth)' | '/(dashboard)' | '/(auth)/download-recovery-key' | '/(auth)/login' | '/(auth)/onboarding' | '/api/$' + | '/(auth)/login/error' | '/(dashboard)/backups/create' | '/(dashboard)/notifications/$notificationId' | '/(dashboard)/notifications/create' @@ -303,6 +333,7 @@ export interface FileRouteTypes { | '/(dashboard)/settings/' | '/(dashboard)/volumes/' | '/(dashboard)/repositories/$repositoryId/edit' + | '/(dashboard)/settings/sso/new' | '/(dashboard)/backups/$backupId/' | '/(dashboard)/repositories/$repositoryId/' | '/(dashboard)/backups/$backupId/$snapshotId/restore' @@ -312,10 +343,8 @@ export interface FileRouteTypes { } export interface RootRouteChildren { IndexRoute: typeof IndexRoute + authRouteRoute: typeof authRouteRouteWithChildren dashboardRouteRoute: typeof dashboardRouteRouteWithChildren - authDownloadRecoveryKeyRoute: typeof authDownloadRecoveryKeyRoute - authLoginRoute: typeof authLoginRoute - authOnboardingRoute: typeof authOnboardingRoute ApiSplatRoute: typeof ApiSplatRoute } @@ -328,6 +357,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof dashboardRouteRouteImport parentRoute: typeof rootRouteImport } + '/(auth)': { + id: '/(auth)' + path: '' + fullPath: '' + preLoaderRoute: typeof authRouteRouteImport + parentRoute: typeof rootRouteImport + } '/': { id: '/' path: '/' @@ -347,21 +383,21 @@ declare module '@tanstack/react-router' { path: '/onboarding' fullPath: '/onboarding' preLoaderRoute: typeof authOnboardingRouteImport - parentRoute: typeof rootRouteImport + parentRoute: typeof authRouteRoute } '/(auth)/login': { id: '/(auth)/login' path: '/login' fullPath: '/login' preLoaderRoute: typeof authLoginRouteImport - parentRoute: typeof rootRouteImport + parentRoute: typeof authRouteRoute } '/(auth)/download-recovery-key': { id: '/(auth)/download-recovery-key' path: '/download-recovery-key' fullPath: '/download-recovery-key' preLoaderRoute: typeof authDownloadRecoveryKeyRouteImport - parentRoute: typeof rootRouteImport + parentRoute: typeof authRouteRoute } '/(dashboard)/volumes/': { id: '/(dashboard)/volumes/' @@ -440,6 +476,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof dashboardBackupsCreateRouteImport parentRoute: typeof dashboardRouteRoute } + '/(auth)/login/error': { + id: '/(auth)/login/error' + path: '/error' + fullPath: '/login/error' + preLoaderRoute: typeof authLoginErrorRouteImport + parentRoute: typeof authLoginRoute + } '/(dashboard)/repositories/$repositoryId/': { id: '/(dashboard)/repositories/$repositoryId/' path: '/repositories/$repositoryId' @@ -454,6 +497,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof dashboardBackupsBackupIdIndexRouteImport parentRoute: typeof dashboardRouteRoute } + '/(dashboard)/settings/sso/new': { + id: '/(dashboard)/settings/sso/new' + path: '/settings/sso/new' + fullPath: '/settings/sso/new' + preLoaderRoute: typeof dashboardSettingsSsoNewRouteImport + parentRoute: typeof dashboardRouteRoute + } '/(dashboard)/repositories/$repositoryId/edit': { id: '/(dashboard)/repositories/$repositoryId/edit' path: '/repositories/$repositoryId/edit' @@ -485,6 +535,34 @@ declare module '@tanstack/react-router' { } } +interface authLoginRouteChildren { + authLoginErrorRoute: typeof authLoginErrorRoute +} + +const authLoginRouteChildren: authLoginRouteChildren = { + authLoginErrorRoute: authLoginErrorRoute, +} + +const authLoginRouteWithChildren = authLoginRoute._addFileChildren( + authLoginRouteChildren, +) + +interface authRouteRouteChildren { + authDownloadRecoveryKeyRoute: typeof authDownloadRecoveryKeyRoute + authLoginRoute: typeof authLoginRouteWithChildren + authOnboardingRoute: typeof authOnboardingRoute +} + +const authRouteRouteChildren: authRouteRouteChildren = { + authDownloadRecoveryKeyRoute: authDownloadRecoveryKeyRoute, + authLoginRoute: authLoginRouteWithChildren, + authOnboardingRoute: authOnboardingRoute, +} + +const authRouteRouteWithChildren = authRouteRoute._addFileChildren( + authRouteRouteChildren, +) + interface dashboardRouteRouteChildren { dashboardBackupsCreateRoute: typeof dashboardBackupsCreateRoute dashboardNotificationsNotificationIdRoute: typeof dashboardNotificationsNotificationIdRoute @@ -498,6 +576,7 @@ interface dashboardRouteRouteChildren { dashboardSettingsIndexRoute: typeof dashboardSettingsIndexRoute dashboardVolumesIndexRoute: typeof dashboardVolumesIndexRoute dashboardRepositoriesRepositoryIdEditRoute: typeof dashboardRepositoriesRepositoryIdEditRoute + dashboardSettingsSsoNewRoute: typeof dashboardSettingsSsoNewRoute dashboardBackupsBackupIdIndexRoute: typeof dashboardBackupsBackupIdIndexRoute dashboardRepositoriesRepositoryIdIndexRoute: typeof dashboardRepositoriesRepositoryIdIndexRoute dashboardBackupsBackupIdSnapshotIdRestoreRoute: typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute @@ -520,6 +599,7 @@ const dashboardRouteRouteChildren: dashboardRouteRouteChildren = { dashboardVolumesIndexRoute: dashboardVolumesIndexRoute, dashboardRepositoriesRepositoryIdEditRoute: dashboardRepositoriesRepositoryIdEditRoute, + dashboardSettingsSsoNewRoute: dashboardSettingsSsoNewRoute, dashboardBackupsBackupIdIndexRoute: dashboardBackupsBackupIdIndexRoute, dashboardRepositoriesRepositoryIdIndexRoute: dashboardRepositoriesRepositoryIdIndexRoute, @@ -537,10 +617,8 @@ const dashboardRouteRouteWithChildren = dashboardRouteRoute._addFileChildren( const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + authRouteRoute: authRouteRouteWithChildren, dashboardRouteRoute: dashboardRouteRouteWithChildren, - authDownloadRecoveryKeyRoute: authDownloadRecoveryKeyRoute, - authLoginRoute: authLoginRoute, - authOnboardingRoute: authOnboardingRoute, ApiSplatRoute: ApiSplatRoute, } export const routeTree = rootRouteImport diff --git a/app/routes/(auth)/login.error.tsx b/app/routes/(auth)/login.error.tsx new file mode 100644 index 00000000..1ba4f47d --- /dev/null +++ b/app/routes/(auth)/login.error.tsx @@ -0,0 +1,23 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { type } from "arktype"; +import { LoginPage } from "~/client/modules/auth/routes/login"; + +export const Route = createFileRoute("/(auth)/login/error")({ + component: RouteComponent, + validateSearch: type({ error: "string?" }), + head: () => ({ + meta: [ + { title: "Zerobyte - Login Error" }, + { + name: "description", + content: "Resolve SSO sign-in errors.", + }, + ], + }), +}); + +function RouteComponent() { + const { error } = Route.useSearch(); + + return ; +} diff --git a/app/routes/(auth)/login.tsx b/app/routes/(auth)/login.tsx index 0734fab4..303cc4ea 100644 --- a/app/routes/(auth)/login.tsx +++ b/app/routes/(auth)/login.tsx @@ -1,8 +1,10 @@ -import { createFileRoute } from "@tanstack/react-router"; +import { Outlet, createFileRoute, useRouterState } from "@tanstack/react-router"; +import { type } from "arktype"; import { LoginPage } from "~/client/modules/auth/routes/login"; export const Route = createFileRoute("/(auth)/login")({ - component: LoginPage, + component: RouteComponent, + validateSearch: type({ error: "string?" }), head: () => ({ meta: [ { title: "Zerobyte - Login" }, @@ -13,3 +15,14 @@ export const Route = createFileRoute("/(auth)/login")({ ], }), }); + +function RouteComponent() { + const { error } = Route.useSearch(); + const pathname = useRouterState({ select: (state) => state.location.pathname }); + + if (pathname !== "/login") { + return ; + } + + return ; +} diff --git a/app/routes/(auth)/route.tsx b/app/routes/(auth)/route.tsx new file mode 100644 index 00000000..e62aea49 --- /dev/null +++ b/app/routes/(auth)/route.tsx @@ -0,0 +1,9 @@ +import { Outlet, createFileRoute } from "@tanstack/react-router"; +import { authMiddleware } from "~/middleware/auth"; + +export const Route = createFileRoute("/(auth)")({ + component: () => , + server: { + middleware: [authMiddleware], + }, +}); diff --git a/app/routes/(dashboard)/settings/index.tsx b/app/routes/(dashboard)/settings/index.tsx index b76fc716..fa491317 100644 --- a/app/routes/(dashboard)/settings/index.tsx +++ b/app/routes/(dashboard)/settings/index.tsx @@ -3,12 +3,21 @@ import { fetchUser } from "../route"; import type { AppContext } from "~/context"; import { SettingsPage } from "~/client/modules/settings/routes/settings"; import { type } from "arktype"; +import { getAdminUsersOptions, getSsoSettingsOptions } from "~/client/api-client/@tanstack/react-query.gen"; export const Route = createFileRoute("/(dashboard)/settings/")({ component: RouteComponent, validateSearch: type({ tab: "string?" }), - loader: async () => { + loader: async ({ context }) => { const authContext = await fetchUser(); + + if (authContext.user?.role === "admin") { + await Promise.all([ + context.queryClient.ensureQueryData(getSsoSettingsOptions()), + context.queryClient.ensureQueryData(getAdminUsersOptions()), + ]); + } + return authContext as AppContext; }, staticData: { diff --git a/app/routes/(dashboard)/settings/sso/new.tsx b/app/routes/(dashboard)/settings/sso/new.tsx new file mode 100644 index 00000000..2ec2907e --- /dev/null +++ b/app/routes/(dashboard)/settings/sso/new.tsx @@ -0,0 +1,32 @@ +import { createFileRoute, redirect } from "@tanstack/react-router"; +import { CreateSsoProviderPage } from "~/client/modules/settings/routes/create-sso-provider"; +import { fetchUser } from "../../route"; + +export const Route = createFileRoute("/(dashboard)/settings/sso/new")({ + component: RouteComponent, + loader: async () => { + const authContext = await fetchUser(); + + if (authContext.user?.role !== "admin") { + throw redirect({ to: "/settings" }); + } + + return authContext; + }, + staticData: { + breadcrumb: () => [{ label: "Settings", href: "/settings" }, { label: "Register SSO Provider" }], + }, + head: () => ({ + meta: [ + { title: "Zerobyte - Register SSO Provider" }, + { + name: "description", + content: "Register a new OIDC identity provider for organization sign-in.", + }, + ], + }), +}); + +function RouteComponent() { + return ; +} diff --git a/app/routes/api.$.ts b/app/routes/api.$.ts index 751a2784..6a88864e 100644 --- a/app/routes/api.$.ts +++ b/app/routes/api.$.ts @@ -8,11 +8,7 @@ const handle = ({ request }: { request: Request }) => app.fetch(request.clone()) export const Route = createFileRoute("/api/$")({ server: { handlers: { - GET: handle, - POST: handle, - DELETE: handle, - PUT: handle, - PATCH: handle, + ANY: handle, }, }, }); diff --git a/app/server/db/relations.ts b/app/server/db/relations.ts index 091e6625..708d9356 100644 --- a/app/server/db/relations.ts +++ b/app/server/db/relations.ts @@ -76,6 +76,7 @@ export const relations = defineRelations(schema, (r) => ({ sessions: r.many.sessionsTable(), members: r.many.member(), twoFactors: r.many.twoFactor(), + ssoProviders: r.many.ssoProvider(), organizations: r.many.organization({ from: r.usersTable.id.through(r.member.userId), to: r.organization.id.through(r.member.organizationId), @@ -104,6 +105,19 @@ export const relations = defineRelations(schema, (r) => ({ volumes: r.many.volumesTable(), members: r.many.member(), invitations: r.many.invitation(), + ssoProviders: r.many.ssoProvider(), + }, + ssoProvider: { + user: r.one.usersTable({ + from: r.ssoProvider.userId, + to: r.usersTable.id, + optional: true, + }), + organization: r.one.organization({ + from: r.ssoProvider.organizationId, + to: r.organization.id, + optional: false, + }), }, volumesTable: { backupSchedules: r.many.backupSchedulesTable(), diff --git a/app/server/db/schema.ts b/app/server/db/schema.ts index 94041fdc..da5873f5 100644 --- a/app/server/db/schema.ts +++ b/app/server/db/schema.ts @@ -173,6 +173,27 @@ export const invitation = sqliteTable( ], ); +export const ssoProvider = sqliteTable("sso_provider", { + id: text("id").primaryKey(), + providerId: text("provider_id").notNull().unique(), + organizationId: text("organization_id") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), + userId: text("user_id").references(() => usersTable.id, { onDelete: "set null" }), + issuer: text("issuer").notNull(), + domain: text("domain").notNull(), + autoLinkMatchingEmails: int("auto_link_matching_emails", { mode: "boolean" }).notNull().default(false), + oidcConfig: text("oidc_config", { mode: "json" }).$type | null>(), + samlConfig: text("saml_config", { mode: "json" }).$type | null>(), + createdAt: integer("created_at", { mode: "timestamp_ms" }) + .notNull() + .default(sql`(unixepoch() * 1000)`), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }) + .notNull() + .$onUpdate(() => new Date()) + .default(sql`(unixepoch() * 1000)`), +}); + /** * Volumes Table */ diff --git a/app/server/lib/auth.ts b/app/server/lib/auth.ts index ee8f63db..5fe675b3 100644 --- a/app/server/lib/auth.ts +++ b/app/server/lib/auth.ts @@ -4,20 +4,24 @@ import { type BetterAuthOptions, type MiddlewareContext, type MiddlewareOptions, + type User, } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { admin, createAuthMiddleware, twoFactor, username, organization } from "better-auth/plugins"; -import { UnauthorizedError } from "http-errors-enhanced"; -import { convertLegacyUserOnFirstLogin } from "./auth-middlewares/convert-legacy-user"; -import { eq } from "drizzle-orm"; +import { sso } from "@better-auth/sso"; import { config } from "../core/config"; import { db } from "../db/db"; import { cryptoUtils } from "../utils/crypto"; -import { organization as organizationTable, member, usersTable } from "../db/schema"; -import { ensureOnlyOneUser } from "./auth-middlewares/only-one-user"; import { authService } from "../modules/auth/auth.service"; import { tanstackStartCookies } from "better-auth/tanstack-start"; import { isValidUsername, normalizeUsername } from "~/lib/username"; +import { ensureOnlyOneUser } from "./auth/middlewares/only-one-user"; +import { convertLegacyUserOnFirstLogin } from "./auth/middlewares/convert-legacy-user"; +import { validateSsoCallbackUrls } from "./auth/middlewares/validate-sso-callback-urls"; +import { validateSsoProviderId } from "./auth/middlewares/validate-sso-provider-id"; +import { createUserDefaultOrg } from "./auth/helpers/create-default-org"; +import { isSsoCallbackRequest, requireSsoInvitation } from "./auth/middlewares/require-sso-invitation"; +import { ssoTrustedProviderLinkingPlugin } from "./auth/plugins/sso-trusted-provider-linking"; export type AuthMiddlewareContext = MiddlewareContext>; @@ -34,6 +38,8 @@ export const auth = betterAuth({ }, hooks: { before: createAuthMiddleware(async (ctx) => { + await validateSsoProviderId(ctx); + await validateSsoCallbackUrls(ctx); await ensureOnlyOneUser(ctx); await convertLegacyUserOnFirstLogin(ctx); }), @@ -49,7 +55,12 @@ export const auth = betterAuth({ }, }, create: { - before: async (user) => { + before: async (user, ctx) => { + if (isSsoCallbackRequest(ctx)) { + await requireSsoInvitation(user.email, ctx); + user.hasDownloadedResticPassword = true; + } + const anyUser = await db.query.usersTable.findFirst(); const isFirstUser = !anyUser; @@ -57,65 +68,19 @@ export const auth = betterAuth({ user.role = "admin"; } - return { data: user }; - }, - after: async (user) => { - const slug = user.email.split("@")[0] + "-" + Math.random().toString(36).slice(-4); - - const resticPassword = cryptoUtils.generateResticPassword(); - const metadata = { - resticPassword: await cryptoUtils.sealSecret(resticPassword), - }; - - try { - db.transaction((tx) => { - const orgId = Bun.randomUUIDv7(); - - tx.insert(organizationTable) - .values({ - name: `${user.name}'s Workspace`, - slug: slug, - id: orgId, - createdAt: new Date(), - metadata, - }) - .run(); - - tx.insert(member) - .values({ - id: Bun.randomUUIDv7(), - userId: user.id, - role: "owner", - organizationId: orgId, - createdAt: new Date(), - }) - .run(); - }); - } catch { - await db.delete(usersTable).where(eq(usersTable.id, user.id)); - - throw new Error(`Failed to create organization for user ${user.id}`); + if (!user.username) { + user.username = Bun.randomUUIDv7(); } + + return { data: user }; }, }, }, session: { create: { - before: async (session) => { - const orgMembership = await db.query.member.findFirst({ - where: { userId: session.userId }, - }); - - if (!orgMembership) { - throw new UnauthorizedError("User does not belong to any organization"); - } - - return { - data: { - ...session, - activeOrganizationId: orgMembership?.organizationId, - }, - }; + before: async (session, ctx) => { + const membership = await createUserDefaultOrg(session.userId, ctx); + return { data: { ...session, activeOrganizationId: membership.organizationId } }; }, }, }, @@ -123,6 +88,11 @@ export const auth = betterAuth({ emailAndPassword: { enabled: true, }, + account: { + accountLinking: { + enabled: true, + }, + }, user: { modelName: "usersTable", additionalFields: { @@ -151,6 +121,22 @@ export const auth = betterAuth({ organization({ allowUserToCreateOrganization: false, }), + sso({ + trustEmailVerified: false, + providersLimit: async (user: User) => { + const existingUser = await db.query.usersTable.findFirst({ + columns: { role: true }, + where: { id: user.id }, + }); + + return existingUser?.role === "admin" ? 10 : 0; + }, + organizationProvisioning: { + disabled: false, + defaultRole: "member", + }, + }), + ssoTrustedProviderLinkingPlugin(), twoFactor({ backupCodeOptions: { storeBackupCodes: "encrypted", diff --git a/app/server/lib/auth/helpers/__tests__/create-default-org.test.ts b/app/server/lib/auth/helpers/__tests__/create-default-org.test.ts new file mode 100644 index 00000000..7c649f0d --- /dev/null +++ b/app/server/lib/auth/helpers/__tests__/create-default-org.test.ts @@ -0,0 +1,161 @@ +import { beforeEach, describe, expect, test } from "bun:test"; +import type { GenericEndpointContext } from "@better-auth/core"; +import { eq } from "drizzle-orm"; +import { db } from "~/server/db/db"; +import { account, invitation, member, organization, ssoProvider, usersTable } from "~/server/db/schema"; +import { createUserDefaultOrg } from "../create-default-org"; + +function createMockContext(path: string, params: Record = {}): GenericEndpointContext { + return { + path, + body: {}, + query: {}, + headers: new Headers(), + request: new Request(`http://localhost:3000${path}`), + params, + method: "POST", + context: {} as GenericEndpointContext["context"], + } as GenericEndpointContext; +} + +function createMockSsoCallbackContext(providerId: string): GenericEndpointContext { + return createMockContext(`/sso/callback/${providerId}`, { providerId }); +} + +function randomId() { + return Bun.randomUUIDv7(); +} + +function randomSlug(prefix: string) { + return `${prefix}-${Math.random().toString(36).slice(2, 8)}`; +} + +async function createUser(email: string, username: string) { + const userId = randomId(); + await db.insert(usersTable).values({ + id: userId, + email, + name: username, + username, + }); + return userId; +} + +describe("createUserDefaultOrg", () => { + beforeEach(async () => { + await db.delete(member); + await db.delete(account); + await db.delete(invitation); + await db.delete(ssoProvider); + await db.delete(organization); + await db.delete(usersTable); + }); + + test("creates invited membership from SSO callback request context", async () => { + const invitedUserId = await createUser("invited@example.com", randomSlug("invited")); + const inviterId = await createUser("inviter@example.com", randomSlug("inviter")); + const organizationId = randomId(); + + await db.insert(organization).values({ + id: organizationId, + name: "Acme", + slug: randomSlug("acme"), + createdAt: new Date(), + }); + + await db.insert(ssoProvider).values({ + id: randomId(), + providerId: "oidc-acme", + organizationId, + userId: inviterId, + issuer: "https://issuer.example.com", + domain: "example.com", + }); + + await db.insert(invitation).values({ + id: randomId(), + organizationId, + email: "invited@example.com", + role: "member", + status: "pending", + expiresAt: new Date(Date.now() + 60 * 60 * 1000), + createdAt: new Date(), + inviterId, + }); + + const ctx = createMockSsoCallbackContext("oidc-acme"); + const membership = await createUserDefaultOrg(invitedUserId, ctx); + + expect(membership.organizationId).toBe(organizationId); + expect(membership.role).toBe("member"); + + const updatedInvitations = await db.select().from(invitation).where(eq(invitation.organizationId, organizationId)); + const updatedInvitation = updatedInvitations.find((i) => i.email === "invited@example.com"); + expect(updatedInvitation?.status).toBe("accepted"); + }); + + test("blocks SSO callback users without pending invitations", async () => { + const userId = await createUser("new-user@example.com", randomSlug("new-user")); + const inviterId = await createUser("inviter@example.com", randomSlug("inviter")); + const organizationId = randomId(); + + await db.insert(organization).values({ + id: organizationId, + name: "Acme", + slug: randomSlug("acme"), + createdAt: new Date(), + }); + + await db.insert(ssoProvider).values({ + id: randomId(), + providerId: "oidc-acme", + organizationId, + userId: inviterId, + issuer: "https://issuer.example.com", + domain: "example.com", + }); + + const ctx = createMockSsoCallbackContext("oidc-acme"); + expect(createUserDefaultOrg(userId, ctx)).rejects.toThrow("invite-only"); + }); + + test("returns existing membership without creating another workspace", async () => { + const userId = await createUser("existing-member@example.com", randomSlug("existing-member")); + const organizationId = randomId(); + + await db.insert(organization).values({ + id: organizationId, + name: "Existing Org", + slug: randomSlug("existing"), + createdAt: new Date(), + }); + + await db.insert(member).values({ + id: randomId(), + userId, + organizationId, + role: "owner", + createdAt: new Date(), + }); + + const membership = await createUserDefaultOrg(userId, null); + + expect(membership.organizationId).toBe(organizationId); + expect(membership.role).toBe("owner"); + + const memberships = await db.select().from(member).where(eq(member.userId, userId)); + expect(memberships.length).toBe(1); + + const organizations = await db.select().from(organization); + expect(organizations.length).toBe(1); + }); + + test("creates personal workspace for non-SSO flows", async () => { + const userId = await createUser("local-user@example.com", randomSlug("local-user")); + + const membership = await createUserDefaultOrg(userId, null); + + expect(membership.role).toBe("owner"); + expect(membership.organization.name).toContain("Workspace"); + }); +}); diff --git a/app/server/lib/auth/helpers/create-default-org.ts b/app/server/lib/auth/helpers/create-default-org.ts new file mode 100644 index 00000000..ca8a1551 --- /dev/null +++ b/app/server/lib/auth/helpers/create-default-org.ts @@ -0,0 +1,172 @@ +import { and, eq, gt } from "drizzle-orm"; +import { UnauthorizedError } from "http-errors-enhanced"; +import type { GenericEndpointContext } from "@better-auth/core"; +import { db } from "~/server/db/db"; +import { invitation, member, organization, ssoProvider, usersTable, type User } from "~/server/db/schema"; +import { cryptoUtils } from "~/server/utils/crypto"; +import { APIError } from "better-auth"; +import { extractProviderIdFromContext, normalizeEmail } from "../utils/sso-context"; +import { logger } from "~/server/utils/logger"; + +export async function findMembershipWithOrganization(userId: string, organizationId?: string) { + const memberships = await db + .select() + .from(member) + .where( + organizationId + ? and(eq(member.userId, userId), eq(member.organizationId, organizationId)) + : eq(member.userId, userId), + ) + .limit(1); + + const membership = memberships[0]; + + if (!membership) { + return null; + } + + const orgs = await db.select().from(organization).where(eq(organization.id, membership.organizationId)).limit(1); + const org = orgs[0]; + + if (!org) { + return null; + } + + return { ...membership, organization: org }; +} + +function buildOrgSlug(email: string) { + const [emailPrefix] = email.split("@"); + const sanitized = emailPrefix + .toLowerCase() + .replace(/[^a-z0-9-]+/g, "-") + .replace(/-+/g, "-") + .replace(/^-+|-+$/g, ""); + const safePrefix = sanitized || "org"; + return `${safePrefix}-${Math.random().toString(36).slice(-4)}`; +} + +async function tryCreateInvitedMembership(userId: string, email: string, ctx: GenericEndpointContext | null) { + logger.debug("Checking for pending invitations for user", userId); + + const providerId = extractProviderIdFromContext(ctx); + const ssoProviders = await db.select().from(ssoProvider).where(eq(ssoProvider.providerId, providerId)).limit(1); + const ssoProviderRecord = ssoProviders[0]; + + if (!ssoProviderRecord) { + logger.debug("No SSO provider found in context, skipping invitation check"); + return null; + } + logger.debug("SSO provider found in context, checking for linked accounts", ssoProviderRecord.providerId); + + const now = new Date(); + + const pendingInvitations = await db + .select({ + id: invitation.id, + email: invitation.email, + role: invitation.role, + organizationId: invitation.organizationId, + }) + .from(invitation) + .where( + and( + eq(invitation.status, "pending"), + eq(invitation.organizationId, ssoProviderRecord.organizationId), + gt(invitation.expiresAt, now), + eq(invitation.email, normalizeEmail(email)), + ), + ) + .limit(1); + const pendingInvitation = pendingInvitations[0]; + + if (!pendingInvitation) { + logger.debug("No pending invitation found for user"); + throw new APIError("FORBIDDEN", { message: "SSO sign-in is invite-only for this organization" }); + } + + await db.transaction(async (tx) => { + tx.insert(member) + .values({ + id: Bun.randomUUIDv7(), + userId, + role: pendingInvitation.role as "member", + organizationId: pendingInvitation.organizationId, + createdAt: new Date(), + }) + .run(); + + tx.update(invitation).set({ status: "accepted" }).where(eq(invitation.id, pendingInvitation.id)).run(); + }); + + const invitedMembership = await findMembershipWithOrganization(userId, pendingInvitation.organizationId); + logger.debug("Created organization membership from invitation", { + userId, + organizationId: pendingInvitation.organizationId, + }); + + if (!invitedMembership) { + throw new Error("Failed to create invited organization membership"); + } + + return invitedMembership; +} + +async function createDefaultOrganizationMembership(user: User) { + logger.debug("Creating default organization for user", { userId: user.id }); + const resticPassword = cryptoUtils.generateResticPassword(); + const metadata = { resticPassword: await cryptoUtils.sealSecret(resticPassword) }; + + await db.transaction(async (tx) => { + const orgId = Bun.randomUUIDv7(); + const slug = buildOrgSlug(user.email); + + tx.insert(organization) + .values({ + name: `${user.name}'s Workspace`, + slug, + id: orgId, + createdAt: new Date(), + metadata, + }) + .run(); + + tx.insert(member) + .values({ + id: Bun.randomUUIDv7(), + userId: user.id, + role: "owner", + organizationId: orgId, + createdAt: new Date(), + }) + .run(); + }); +} + +export async function createUserDefaultOrg(userId: string, ctx: GenericEndpointContext | null) { + const users = await db.select().from(usersTable).where(eq(usersTable.id, userId)).limit(1); + const user = users[0]; + if (!user) { + throw new UnauthorizedError("User not found"); + } + + const existingMembership = await findMembershipWithOrganization(user.id); + if (existingMembership) { + logger.debug("User already has an organization membership, skipping default org creation", { userId }); + return existingMembership; + } + + const invitedMembership = await tryCreateInvitedMembership(userId, normalizeEmail(user.email), ctx); + if (invitedMembership) { + return invitedMembership; + } + + await createDefaultOrganizationMembership(user); + + const newMembership = await findMembershipWithOrganization(userId); + if (!newMembership) { + throw new Error("Failed to create default organization"); + } + + return newMembership; +} diff --git a/app/server/lib/auth-middlewares/__tests__/convert-legacy-user.test.ts b/app/server/lib/auth/middlewares/__tests__/convert-legacy-user.test.ts similarity index 97% rename from app/server/lib/auth-middlewares/__tests__/convert-legacy-user.test.ts rename to app/server/lib/auth/middlewares/__tests__/convert-legacy-user.test.ts index ffa6009c..158f72b7 100644 --- a/app/server/lib/auth-middlewares/__tests__/convert-legacy-user.test.ts +++ b/app/server/lib/auth/middlewares/__tests__/convert-legacy-user.test.ts @@ -1,8 +1,8 @@ -import { test, describe, mock, beforeEach, afterEach, expect } from "bun:test"; +import { test, describe, beforeEach, expect } from "bun:test"; import { convertLegacyUserOnFirstLogin } from "../convert-legacy-user"; import { db } from "~/server/db/db"; import { usersTable, account, organization, member } from "~/server/db/schema"; -import type { AuthMiddlewareContext } from "../../auth"; +import type { AuthMiddlewareContext } from "~/server/lib/auth"; describe("convertLegacyUserOnFirstLogin", () => { beforeEach(async () => { @@ -12,10 +12,6 @@ describe("convertLegacyUserOnFirstLogin", () => { await db.delete(usersTable); }); - afterEach(() => { - mock.restore(); - }); - const createContext = (path: string, body: Record) => ({ path, body }) as AuthMiddlewareContext; test("should return early for non-sign-in paths", async () => { diff --git a/app/server/lib/auth/middlewares/__tests__/require-sso-invitation.test.ts b/app/server/lib/auth/middlewares/__tests__/require-sso-invitation.test.ts new file mode 100644 index 00000000..4c43386f --- /dev/null +++ b/app/server/lib/auth/middlewares/__tests__/require-sso-invitation.test.ts @@ -0,0 +1,142 @@ +import { beforeEach, describe, expect, test } from "bun:test"; +import type { GenericEndpointContext } from "@better-auth/core"; +import { db } from "~/server/db/db"; +import { account, invitation, member, organization, ssoProvider, usersTable } from "~/server/db/schema"; +import { isSsoCallbackRequest, requireSsoInvitation } from "../require-sso-invitation"; + +function createMockContext(path: string, params: Record = {}): GenericEndpointContext { + return { + path, + body: {}, + query: {}, + headers: new Headers(), + request: new Request(`http://test.local${path}`), + params, + method: "POST", + context: {} as GenericEndpointContext["context"], + } as GenericEndpointContext; +} + +function createMockSsoCallbackContext(providerId: string): GenericEndpointContext { + return createMockContext(`/sso/callback/${providerId}`, { providerId }); +} + +function randomId() { + return Bun.randomUUIDv7(); +} + +function randomSlug(prefix: string) { + return `${prefix}-${Math.random().toString(36).slice(2, 8)}`; +} + +async function createSsoProvider(providerId: string) { + const inviterId = randomId(); + const organizationId = randomId(); + + await db.insert(usersTable).values({ + id: inviterId, + username: randomSlug("inviter"), + email: `${randomSlug("inviter")}@example.com`, + name: "Inviter", + }); + + await db.insert(organization).values({ + id: organizationId, + name: "Acme", + slug: randomSlug("acme"), + createdAt: new Date(), + }); + + await db.insert(ssoProvider).values({ + id: randomId(), + providerId, + organizationId, + userId: inviterId, + issuer: "https://issuer.example.com", + domain: "example.com", + }); + + return { inviterId, organizationId }; +} + +describe("requireSsoInvitation", () => { + beforeEach(async () => { + await db.delete(member); + await db.delete(account); + await db.delete(invitation); + await db.delete(ssoProvider); + await db.delete(organization); + await db.delete(usersTable); + }); + + test("throws when context is null", async () => { + await createSsoProvider("oidc-acme"); + + expect(requireSsoInvitation("user@example.com", null)).rejects.toThrow("Missing SSO context"); + }); + + test("throws when request is not an SSO callback", async () => { + await createSsoProvider("oidc-acme"); + + const ctx = createMockContext("/sign-up/email"); + expect(requireSsoInvitation("user@example.com", ctx)).rejects.toThrow("Missing providerId"); + }); + + test("detects whether current request is an SSO callback", async () => { + expect(isSsoCallbackRequest(null)).toBe(false); + + const nonSsoResult = isSsoCallbackRequest(createMockContext("/sign-up/email")); + expect(nonSsoResult).toBe(false); + + const ssoResult = isSsoCallbackRequest(createMockSsoCallbackContext("oidc-acme")); + expect(ssoResult).toBe(true); + }); + + test("blocks SSO callback when no pending invitation exists", async () => { + await createSsoProvider("oidc-acme"); + + const ctx = createMockSsoCallbackContext("oidc-acme"); + expect(requireSsoInvitation("user@example.com", ctx)).rejects.toThrow("must be invited"); + }); + + test("blocks SSO callback when invitation is expired", async () => { + const { inviterId, organizationId } = await createSsoProvider("oidc-acme"); + + await db.insert(invitation).values({ + id: randomId(), + organizationId, + email: "user@example.com", + role: "member", + status: "pending", + expiresAt: new Date(Date.now() - 1_000), + createdAt: new Date(), + inviterId, + }); + + const ctx = createMockSsoCallbackContext("oidc-acme"); + expect(requireSsoInvitation("user@example.com", ctx)).rejects.toThrow("must be invited"); + }); + + test("allows SSO callback when a matching pending invitation exists", async () => { + const { inviterId, organizationId } = await createSsoProvider("oidc-acme"); + + await db.insert(invitation).values({ + id: randomId(), + organizationId, + email: "user@example.com", + role: "member", + status: "pending", + expiresAt: new Date(Date.now() + 60 * 60 * 1000), + createdAt: new Date(), + inviterId, + }); + + const ctx = createMockSsoCallbackContext("oidc-acme"); + expect(requireSsoInvitation(" USER@EXAMPLE.COM ", ctx)).resolves.toBeUndefined(); + }); + + test("throws when provider is not registered", async () => { + const ctx = createMockSsoCallbackContext("missing-provider"); + expect(requireSsoInvitation("user@example.com", ctx)).rejects.toThrow("SSO provider not found"); + }); +}); diff --git a/app/server/lib/auth/middlewares/__tests__/trust-sso-provider-for-linking.test.ts b/app/server/lib/auth/middlewares/__tests__/trust-sso-provider-for-linking.test.ts new file mode 100644 index 00000000..6c487414 --- /dev/null +++ b/app/server/lib/auth/middlewares/__tests__/trust-sso-provider-for-linking.test.ts @@ -0,0 +1,215 @@ +import { beforeEach, describe, expect, test } from "bun:test"; +import type { GenericEndpointContext } from "@better-auth/core"; +import { eq } from "drizzle-orm"; +import { db } from "~/server/db/db"; +import { account, member, organization, ssoProvider, usersTable } from "~/server/db/schema"; +import { isSsoCallbackPath, trustSsoProviderForLinking } from "../trust-sso-provider-for-linking"; + +function randomId() { + return Bun.randomUUIDv7(); +} + +function randomSlug(prefix: string) { + return `${prefix}-${Math.random().toString(36).slice(2, 8)}`; +} + +function createMockContext(options: { + path: string; + method?: string; + params?: Record; + trustedProviders?: string[]; + enabled?: boolean; +}): GenericEndpointContext { + const { path, method = "GET", params = {}, trustedProviders = [], enabled = true } = options; + + const accountLinking = { + enabled, + trustedProviders, + }; + + const context = { + options: { + account: { + accountLinking, + }, + }, + }; + + return { + path, + body: {}, + query: {}, + headers: new Headers(), + request: new Request(`http://test.local${path}`, { method }), + params, + method, + context: context as GenericEndpointContext["context"], + } as GenericEndpointContext; +} + +async function createSsoProviderRecord( + providerId: string, + autoLinkMatchingEmails: boolean, + options: { organizationId?: string; userId?: string } = {}, +) { + const userId = options.userId ?? randomId(); + const organizationId = options.organizationId ?? randomId(); + + if (!options.userId) { + await db.insert(usersTable).values({ + id: userId, + username: randomSlug("inviter"), + email: `${randomSlug("inviter")}@example.com`, + name: "Inviter", + }); + } + + if (!options.organizationId) { + await db.insert(organization).values({ + id: organizationId, + name: "Acme", + slug: randomSlug("acme"), + createdAt: new Date(), + }); + } + + await db.insert(ssoProvider).values({ + id: randomId(), + providerId, + organizationId, + userId, + issuer: "https://issuer.example.com", + domain: "example.com", + autoLinkMatchingEmails, + }); + + return { organizationId, userId }; +} + +describe("isSsoCallbackPath", () => { + test("detects OIDC callback paths", () => { + expect(isSsoCallbackPath("/sso/callback/pocket-id")).toBe(true); + }); + + test("ignores non-callback paths", () => { + expect(isSsoCallbackPath("/sso/register")).toBe(false); + expect(isSsoCallbackPath("/sign-in/email")).toBe(false); + }); +}); + +describe("trustSsoProviderForLinking", () => { + beforeEach(async () => { + await db.delete(member); + await db.delete(account); + await db.delete(ssoProvider); + await db.delete(organization); + await db.delete(usersTable); + }); + + test("adds callback provider to trusted providers", async () => { + await createSsoProviderRecord("pocket-id", true); + + const ctx = createMockContext({ + path: "/sso/callback/pocket-id", + params: { providerId: "pocket-id" }, + }); + + await trustSsoProviderForLinking(ctx); + + expect(ctx.context.options.account?.accountLinking?.trustedProviders).toContain("pocket-id"); + }); + + test("does not trust providers with disabled auto-linking", async () => { + await createSsoProviderRecord("pocket-id", false); + + const ctx = createMockContext({ + path: "/sso/callback/pocket-id", + params: { providerId: "pocket-id" }, + }); + + await trustSsoProviderForLinking(ctx); + + expect(ctx.context.options.account?.accountLinking?.trustedProviders).toEqual([]); + }); + + test("replaces stale trusted providers with database state", async () => { + await createSsoProviderRecord("pocket-id", true); + + const ctx = createMockContext({ + path: "/sso/callback/pocket-id", + params: { providerId: "pocket-id" }, + trustedProviders: ["stale-provider"], + }); + + await trustSsoProviderForLinking(ctx); + + expect(ctx.context.options.account?.accountLinking?.trustedProviders).toEqual(["pocket-id"]); + }); + + test("does not trust unknown providers", async () => { + const ctx = createMockContext({ + path: "/sso/callback/missing-provider", + params: { providerId: "missing-provider" }, + }); + + await trustSsoProviderForLinking(ctx); + + expect(ctx.context.options.account?.accountLinking?.trustedProviders).toEqual([]); + }); + + test("does not duplicate an existing provider", async () => { + await createSsoProviderRecord("pocket-id", true); + + const ctx = createMockContext({ + path: "/sso/callback/pocket-id", + params: { providerId: "pocket-id" }, + trustedProviders: ["pocket-id"], + }); + + await trustSsoProviderForLinking(ctx); + + expect(ctx.context.options.account?.accountLinking?.trustedProviders).toEqual(["pocket-id"]); + }); + + test("removes provider from trusted providers when auto-linking is disabled", async () => { + await createSsoProviderRecord("pocket-id", true); + + const ctx = createMockContext({ + path: "/sso/callback/pocket-id", + params: { providerId: "pocket-id" }, + }); + + await trustSsoProviderForLinking(ctx); + expect(ctx.context.options.account?.accountLinking?.trustedProviders).toEqual(["pocket-id"]); + + await db.update(ssoProvider).set({ autoLinkMatchingEmails: false }).where(eq(ssoProvider.providerId, "pocket-id")); + + await trustSsoProviderForLinking(ctx); + expect(ctx.context.options.account?.accountLinking?.trustedProviders).toEqual([]); + }); + + test("does nothing when account linking is disabled", async () => { + await createSsoProviderRecord("pocket-id", true); + + const ctx = createMockContext({ + path: "/sso/callback/pocket-id", + params: { providerId: "pocket-id" }, + enabled: false, + }); + + await trustSsoProviderForLinking(ctx); + + expect(ctx.context.options.account?.accountLinking?.trustedProviders).toEqual([]); + }); + + test("does nothing when provider id cannot be extracted", async () => { + const ctx = createMockContext({ + path: "/sso/callback/", + params: {}, + }); + + await trustSsoProviderForLinking(ctx); + + expect(ctx.context.options.account?.accountLinking?.trustedProviders).toEqual([]); + }); +}); diff --git a/app/server/lib/auth/middlewares/__tests__/validate-sso-callback-urls.test.ts b/app/server/lib/auth/middlewares/__tests__/validate-sso-callback-urls.test.ts new file mode 100644 index 00000000..c8e3524b --- /dev/null +++ b/app/server/lib/auth/middlewares/__tests__/validate-sso-callback-urls.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, test } from "bun:test"; +import type { AuthMiddlewareContext } from "~/server/lib/auth"; +import { validateSsoCallbackUrls } from "../validate-sso-callback-urls"; + +function createContext( + path: string, + body: Record = {}, + query: Record = {}, +): AuthMiddlewareContext { + return { + path, + body, + query, + headers: new Headers(), + request: new Request(`http://localhost:3000${path}`), + params: {}, + method: "POST", + context: {} as AuthMiddlewareContext["context"], + } as AuthMiddlewareContext; +} + +describe("validateSsoCallbackUrls", () => { + test("accepts relative paths for every callback field", async () => { + const ctx = createContext("/sign-in/sso", { + callbackURL: "/login", + errorCallbackURL: "/login/error", + newUserCallbackURL: "/download-recovery-key", + }); + + expect(validateSsoCallbackUrls(ctx)).resolves.toBeUndefined(); + }); + + test("rejects https://evil.example", async () => { + const ctx = createContext("/sign-in/sso", { callbackURL: "https://evil.example" }); + + expect(validateSsoCallbackUrls(ctx)).rejects.toThrow("callbackURL"); + }); + + test("rejects //evil.example", async () => { + const ctx = createContext("/sign-in/sso", { callbackURL: "//evil.example" }); + + expect(validateSsoCallbackUrls(ctx)).rejects.toThrow("callbackURL"); + }); + + test("rejects /sso/callback/foo", async () => { + const ctx = createContext("/sign-in/sso", { callbackURL: "/sso/callback/foo" }); + + expect(validateSsoCallbackUrls(ctx)).rejects.toThrow("callbackURL"); + }); + + test("rejects /sso/saml2/foo", async () => { + const ctx = createContext("/sign-in/sso", { callbackURL: "/sso/saml2/foo" }); + + expect(validateSsoCallbackUrls(ctx)).rejects.toThrow("callbackURL"); + }); + + test("rejects malicious query callback fields", async () => { + const ctx = createContext("/sign-in/sso", {}, { errorCallbackURL: "https://evil.example" }); + + expect(validateSsoCallbackUrls(ctx)).rejects.toThrow("errorCallbackURL"); + }); + + test("rejects malicious newUserCallbackURL", async () => { + const ctx = createContext("/sign-in/sso", { newUserCallbackURL: "https://evil.example" }); + + expect(validateSsoCallbackUrls(ctx)).rejects.toThrow("newUserCallbackURL"); + }); + + test("skips validation outside SSO sign-in endpoint", async () => { + const ctx = createContext("/sign-in/email", { callbackURL: "https://evil.example" }); + + expect(validateSsoCallbackUrls(ctx)).resolves.toBeUndefined(); + }); +}); diff --git a/app/server/lib/auth/middlewares/__tests__/validate-sso-provider-id.test.ts b/app/server/lib/auth/middlewares/__tests__/validate-sso-provider-id.test.ts new file mode 100644 index 00000000..da5bbc4d --- /dev/null +++ b/app/server/lib/auth/middlewares/__tests__/validate-sso-provider-id.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, test } from "bun:test"; +import type { AuthMiddlewareContext } from "~/server/lib/auth"; +import { validateSsoProviderId } from "../validate-sso-provider-id"; + +function createContext(path: string, body: Record = {}): AuthMiddlewareContext { + return { + path, + body, + query: {}, + headers: new Headers(), + request: new Request(`http://localhost:3000${path}`), + params: {}, + method: "POST", + context: {} as AuthMiddlewareContext["context"], + } as AuthMiddlewareContext; +} + +describe("validateSsoProviderId", () => { + test("allows non-reserved provider id", async () => { + const ctx = createContext("/sso/register", { providerId: "acme-oidc" }); + + expect(validateSsoProviderId(ctx)).resolves.toBeUndefined(); + }); + + test("rejects reserved credential provider id", async () => { + const ctx = createContext("/sso/register", { providerId: "credential" }); + + expect(validateSsoProviderId(ctx)).rejects.toThrow("reserved"); + }); + + test("rejects reserved credentials provider id case-insensitively", async () => { + const ctx = createContext("/sso/register", { providerId: " Credential " }); + + expect(validateSsoProviderId(ctx)).rejects.toThrow("reserved"); + }); + + test("skips validation outside register endpoint", async () => { + const ctx = createContext("/sign-in/sso", { providerId: "credential" }); + + expect(validateSsoProviderId(ctx)).resolves.toBeUndefined(); + }); +}); diff --git a/app/server/lib/auth-middlewares/convert-legacy-user.ts b/app/server/lib/auth/middlewares/convert-legacy-user.ts similarity index 98% rename from app/server/lib/auth-middlewares/convert-legacy-user.ts rename to app/server/lib/auth/middlewares/convert-legacy-user.ts index 73de8227..1cd2514e 100644 --- a/app/server/lib/auth-middlewares/convert-legacy-user.ts +++ b/app/server/lib/auth/middlewares/convert-legacy-user.ts @@ -2,7 +2,7 @@ import { hashPassword } from "better-auth/crypto"; import { eq } from "drizzle-orm"; import { db } from "~/server/db/db"; import { account, usersTable, member, organization } from "~/server/db/schema"; -import type { AuthMiddlewareContext } from "../auth"; +import type { AuthMiddlewareContext } from "~/server/lib/auth"; import { UnauthorizedError } from "http-errors-enhanced"; import { cryptoUtils } from "~/server/utils/crypto"; import { normalizeUsername } from "~/lib/username"; diff --git a/app/server/lib/auth-middlewares/only-one-user.ts b/app/server/lib/auth/middlewares/only-one-user.ts similarity index 92% rename from app/server/lib/auth-middlewares/only-one-user.ts rename to app/server/lib/auth/middlewares/only-one-user.ts index 67b5bc55..d4fc2ca9 100644 --- a/app/server/lib/auth-middlewares/only-one-user.ts +++ b/app/server/lib/auth/middlewares/only-one-user.ts @@ -1,17 +1,18 @@ import { db } from "~/server/db/db"; -import type { AuthMiddlewareContext } from "../auth"; +import type { AuthMiddlewareContext } from "~/server/lib/auth"; import { logger } from "~/server/utils/logger"; import { ForbiddenError } from "http-errors-enhanced"; import { REGISTRATION_ENABLED_KEY } from "~/server/core/constants"; export const ensureOnlyOneUser = async (ctx: AuthMiddlewareContext) => { const { path } = ctx; - const existingUser = await db.query.usersTable.findFirst(); if (path !== "/sign-up/email") { return; } + const existingUser = await db.query.usersTable.findFirst(); + const result = await db.query.appMetadataTable.findFirst({ where: { key: REGISTRATION_ENABLED_KEY }, }); diff --git a/app/server/lib/auth/middlewares/require-sso-invitation.ts b/app/server/lib/auth/middlewares/require-sso-invitation.ts new file mode 100644 index 00000000..fef083f7 --- /dev/null +++ b/app/server/lib/auth/middlewares/require-sso-invitation.ts @@ -0,0 +1,52 @@ +import { APIError } from "better-auth/api"; +import type { GenericEndpointContext } from "@better-auth/core"; +import { db } from "~/server/db/db"; +import { logger } from "~/server/utils/logger"; +import { extractProviderIdFromContext, extractProviderIdFromUrl, normalizeEmail } from "../utils/sso-context"; + +export function isSsoCallbackRequest(ctx: GenericEndpointContext | null) { + if (!ctx?.request?.url) { + return false; + } + + return extractProviderIdFromUrl(ctx.request.url) !== null; +} + +export const requireSsoInvitation = async (userEmail: string, ctx: GenericEndpointContext | null) => { + if (!ctx) { + throw new APIError("BAD_REQUEST", { message: "Missing SSO context" }); + } + + const providerId = extractProviderIdFromContext(ctx); + if (!providerId) { + throw new APIError("BAD_REQUEST", { message: "Missing providerId in context" }); + } + + const provider = await db.query.ssoProvider.findFirst({ where: { providerId } }); + if (!provider) { + throw new APIError("NOT_FOUND", { message: "SSO provider not found" }); + } + + const normalizedEmail = normalizeEmail(userEmail); + logger.debug("Checking for pending invitations", { organizationId: provider.organizationId }); + + const pendingInvitation = await db.query.invitation.findFirst({ + where: { + AND: [ + { organizationId: provider.organizationId }, + { status: "pending" }, + { expiresAt: { gt: new Date() } }, + { email: normalizedEmail }, + ], + }, + columns: { id: true }, + }); + + logger.debug("Pending invitation result", { found: !!pendingInvitation, invitationId: pendingInvitation?.id }); + + if (!pendingInvitation) { + throw new APIError("FORBIDDEN", { + message: "Access denied. You must be invited to this organization before you can sign in with SSO.", + }); + } +}; diff --git a/app/server/lib/auth/middlewares/trust-sso-provider-for-linking.ts b/app/server/lib/auth/middlewares/trust-sso-provider-for-linking.ts new file mode 100644 index 00000000..233dda2f --- /dev/null +++ b/app/server/lib/auth/middlewares/trust-sso-provider-for-linking.ts @@ -0,0 +1,43 @@ +import type { GenericEndpointContext } from "@better-auth/core"; +import { db } from "~/server/db/db"; +import { extractProviderIdFromContext } from "../utils/sso-context"; + +export function isSsoCallbackPath(path?: string): boolean { + if (!path) { + return false; + } + + return path.startsWith("/sso/callback/"); +} + +export async function trustSsoProviderForLinking(ctx: GenericEndpointContext): Promise { + const providerId = extractProviderIdFromContext(ctx); + + if (!providerId) { + return; + } + + const accountLinking = ctx.context.options.account?.accountLinking; + + if (!accountLinking || accountLinking.enabled === false) { + return; + } + + const provider = await db.query.ssoProvider.findFirst({ + columns: { organizationId: true }, + where: { providerId }, + }); + if (!provider) { + return; + } + + const autoLinkingProviders = await db.query.ssoProvider.findMany({ + columns: { providerId: true }, + where: { + organizationId: provider.organizationId, + autoLinkMatchingEmails: true, + }, + }); + + accountLinking.trustedProviders = autoLinkingProviders.map((entry) => entry.providerId); +} diff --git a/app/server/lib/auth/middlewares/validate-sso-callback-urls.ts b/app/server/lib/auth/middlewares/validate-sso-callback-urls.ts new file mode 100644 index 00000000..873f3cdc --- /dev/null +++ b/app/server/lib/auth/middlewares/validate-sso-callback-urls.ts @@ -0,0 +1,36 @@ +import { APIError } from "better-auth/api"; +import type { AuthMiddlewareContext } from "~/server/lib/auth"; + +function isValidCallbackPath(value: string): boolean { + if (!value.startsWith("/") || value.startsWith("//") || value.includes("\\")) { + return false; + } + + if (value.startsWith("/sso/callback/") || value.startsWith("/sso/saml2/")) { + return false; + } + + return true; +} + +export const validateSsoCallbackUrls = async (ctx: AuthMiddlewareContext) => { + if (ctx.path !== "/sign-in/sso") { + return; + } + + const sources = [ctx.body, ctx.query].filter((s) => s && typeof s === "object"); + + for (const source of sources) { + const payload = source as Record; + + for (const field of ["callbackURL", "errorCallbackURL", "newUserCallbackURL"]) { + const value = payload[field]; + + if (value !== undefined && (typeof value !== "string" || !isValidCallbackPath(value))) { + throw new APIError("BAD_REQUEST", { + message: `Invalid ${field}. Only relative paths like /login are allowed.`, + }); + } + } + } +}; diff --git a/app/server/lib/auth/middlewares/validate-sso-provider-id.ts b/app/server/lib/auth/middlewares/validate-sso-provider-id.ts new file mode 100644 index 00000000..903a972c --- /dev/null +++ b/app/server/lib/auth/middlewares/validate-sso-provider-id.ts @@ -0,0 +1,25 @@ +import { APIError } from "better-auth/api"; +import type { AuthMiddlewareContext } from "~/server/lib/auth"; +import { isReservedSsoProviderId } from "../utils/sso-provider-id"; + +export const validateSsoProviderId = async (ctx: AuthMiddlewareContext) => { + if (ctx.path !== "/sso/register") { + return; + } + + if (!ctx.body || typeof ctx.body !== "object") { + return; + } + + const providerId = (ctx.body as Record).providerId; + + if (typeof providerId !== "string") { + return; + } + + if (isReservedSsoProviderId(providerId)) { + throw new APIError("BAD_REQUEST", { + message: `Invalid providerId. '${providerId}' is reserved and cannot be used for SSO providers.`, + }); + } +}; diff --git a/app/server/lib/auth/plugins/sso-trusted-provider-linking.ts b/app/server/lib/auth/plugins/sso-trusted-provider-linking.ts new file mode 100644 index 00000000..196dc03b --- /dev/null +++ b/app/server/lib/auth/plugins/sso-trusted-provider-linking.ts @@ -0,0 +1,21 @@ +import type { BetterAuthPlugin } from "better-auth"; +import { createAuthMiddleware } from "better-auth/api"; +import { isSsoCallbackPath, trustSsoProviderForLinking } from "../middlewares/trust-sso-provider-for-linking"; + +export function ssoTrustedProviderLinkingPlugin(): BetterAuthPlugin { + return { + id: "sso-trusted-provider-linking", + hooks: { + before: [ + { + matcher(context) { + return isSsoCallbackPath(context.path); + }, + handler: createAuthMiddleware(async (ctx) => { + await trustSsoProviderForLinking(ctx); + }), + }, + ], + }, + }; +} diff --git a/app/server/lib/auth/utils/sso-context.ts b/app/server/lib/auth/utils/sso-context.ts new file mode 100644 index 00000000..ab36b14a --- /dev/null +++ b/app/server/lib/auth/utils/sso-context.ts @@ -0,0 +1,31 @@ +import type { GenericEndpointContext } from "@better-auth/core"; + +export function normalizeEmail(email: string): string { + return email.trim().toLowerCase(); +} + +export function extractProviderIdFromUrl(url: string): string | null { + try { + const pathname = new URL(url, "http://localhost").pathname; + const match = pathname.match(/\/sso\/(?:saml2\/)?callback\/([^/]+)$/); + return match?.[1] ?? null; + } catch { + return null; + } +} + +export function extractProviderIdFromContext(ctx?: GenericEndpointContext | null) { + if (!ctx) { + return null; + } + + if (ctx.params?.providerId) { + return ctx.params.providerId; + } + + if (ctx.request?.url) { + return extractProviderIdFromUrl(ctx.request.url); + } + + return null; +} diff --git a/app/server/lib/auth/utils/sso-provider-id.ts b/app/server/lib/auth/utils/sso-provider-id.ts new file mode 100644 index 00000000..b76f8903 --- /dev/null +++ b/app/server/lib/auth/utils/sso-provider-id.ts @@ -0,0 +1,9 @@ +const RESERVED_SSO_PROVIDER_IDS = new Set(["credential"]); + +export function normalizeSsoProviderId(providerId: string): string { + return providerId.trim().toLowerCase(); +} + +export function isReservedSsoProviderId(providerId: string): boolean { + return RESERVED_SSO_PROVIDER_IDS.has(normalizeSsoProviderId(providerId)); +} diff --git a/app/server/lib/functions/organization-context.ts b/app/server/lib/functions/organization-context.ts new file mode 100644 index 00000000..5b46e70f --- /dev/null +++ b/app/server/lib/functions/organization-context.ts @@ -0,0 +1,31 @@ +import { createServerFn } from "@tanstack/react-start"; +import { auth } from "../auth"; +import { getRequest } from "@tanstack/react-start/server"; + +export const getOrganizationContext = createServerFn({ method: "GET" }).handler(async () => { + const request = getRequest(); + + const [data, session] = await Promise.all([ + auth.api.listOrganizations({ + headers: request.headers, + }), + auth.api.getSession({ headers: request.headers }), + ]); + + const activeOrganizationId = session?.session?.activeOrganizationId; + const activeOrganization = data.find((org) => org.id === activeOrganizationId); + + if (data.length === 0) { + throw new Error("No organizations found for user"); + } + + const member = await auth.api.getActiveMember({ + headers: request.headers, + }); + + return { + organizations: data, + activeOrganization: activeOrganization || data[0], + activeMember: member, + }; +}); diff --git a/app/server/modules/auth/__tests__/auth.service.sso-provider.test.ts b/app/server/modules/auth/__tests__/auth.service.sso-provider.test.ts new file mode 100644 index 00000000..cb7f6c8b --- /dev/null +++ b/app/server/modules/auth/__tests__/auth.service.sso-provider.test.ts @@ -0,0 +1,177 @@ +import { beforeEach, describe, expect, test } from "bun:test"; +import { db } from "~/server/db/db"; +import { account, invitation, member, organization, ssoProvider, usersTable } from "~/server/db/schema"; +import { authService } from "../auth.service"; + +function randomId() { + return Bun.randomUUIDv7(); +} + +function randomSlug(prefix: string) { + return `${prefix}-${Math.random().toString(36).slice(2, 8)}`; +} + +async function createUser(email: string) { + const id = randomId(); + + await db.insert(usersTable).values({ + id, + email, + name: email.split("@")[0], + username: randomSlug("user"), + }); + + return id; +} + +async function createOrganization(name: string) { + const id = randomId(); + + await db.insert(organization).values({ + id, + name, + slug: randomSlug("org"), + createdAt: new Date(), + }); + + return id; +} + +describe("authService.deleteSsoProvider", () => { + beforeEach(async () => { + await db.delete(member); + await db.delete(account); + await db.delete(invitation); + await db.delete(ssoProvider); + await db.delete(organization); + await db.delete(usersTable); + }); + + test("does not delete accounts when provider belongs to another organization", async () => { + const orgA = await createOrganization("Org A"); + const orgB = await createOrganization("Org B"); + const providerOwner = await createUser(`${randomSlug("owner")}@example.com`); + const accountUser = await createUser(`${randomSlug("member")}@example.com`); + + const providerId = `oidc-${randomSlug("provider")}`; + + await db.insert(ssoProvider).values({ + id: randomId(), + providerId, + organizationId: orgB, + userId: providerOwner, + issuer: "https://issuer.example.com", + domain: "example.com", + }); + + await db.insert(account).values({ + id: randomId(), + accountId: randomSlug("acct"), + providerId, + userId: accountUser, + }); + + const deleted = await authService.deleteSsoProvider(providerId, orgA); + + expect(deleted).toBe(false); + + const remainingProvider = await db.query.ssoProvider.findFirst({ + where: { providerId }, + columns: { id: true }, + }); + const remainingAccounts = await db.query.account.findMany({ + where: { providerId }, + columns: { id: true }, + }); + + expect(remainingProvider).not.toBeUndefined(); + expect(remainingAccounts).toHaveLength(1); + }); + + test("deletes provider and linked accounts in the active organization", async () => { + const org = await createOrganization("Org A"); + const providerOwner = await createUser(`${randomSlug("owner")}@example.com`); + const accountUserA = await createUser(`${randomSlug("member-a")}@example.com`); + const accountUserB = await createUser(`${randomSlug("member-b")}@example.com`); + + const providerId = `oidc-${randomSlug("provider")}`; + + await db.insert(ssoProvider).values({ + id: randomId(), + providerId, + organizationId: org, + userId: providerOwner, + issuer: "https://issuer.example.com", + domain: "example.com", + }); + + await db.insert(account).values([ + { + id: randomId(), + accountId: randomSlug("acct-a"), + providerId, + userId: accountUserA, + }, + { + id: randomId(), + accountId: randomSlug("acct-b"), + providerId, + userId: accountUserB, + }, + ]); + + const deleted = await authService.deleteSsoProvider(providerId, org); + + expect(deleted).toBe(true); + + const remainingProvider = await db.query.ssoProvider.findFirst({ + where: { providerId }, + columns: { id: true }, + }); + const remainingAccounts = await db.query.account.findMany({ + where: { providerId }, + columns: { id: true }, + }); + + expect(remainingProvider).toBeUndefined(); + expect(remainingAccounts).toHaveLength(0); + }); + + test("deleting a reserved provider id never deletes credential accounts", async () => { + const org = await createOrganization("Org A"); + const providerOwner = await createUser(`${randomSlug("owner")}@example.com`); + const credentialUser = await createUser(`${randomSlug("credential")}@example.com`); + + await db.insert(ssoProvider).values({ + id: randomId(), + providerId: "credential", + organizationId: org, + userId: providerOwner, + issuer: "https://issuer.example.com", + domain: "example.com", + }); + + await db.insert(account).values({ + id: randomId(), + accountId: randomSlug("credential-acct"), + providerId: "credential", + userId: credentialUser, + }); + + const deleted = await authService.deleteSsoProvider("credential", org); + + expect(deleted).toBe(true); + + const remainingProvider = await db.query.ssoProvider.findFirst({ + where: { providerId: "credential" }, + columns: { id: true }, + }); + const remainingAccounts = await db.query.account.findMany({ + where: { providerId: "credential" }, + columns: { id: true }, + }); + + expect(remainingProvider).toBeUndefined(); + expect(remainingAccounts).toHaveLength(1); + }); +}); diff --git a/app/server/modules/auth/__tests__/auth.sso-security.test.ts b/app/server/modules/auth/__tests__/auth.sso-security.test.ts new file mode 100644 index 00000000..feb067f9 --- /dev/null +++ b/app/server/modules/auth/__tests__/auth.sso-security.test.ts @@ -0,0 +1,94 @@ +import { beforeEach, describe, expect, test } from "bun:test"; +import { createApp } from "~/server/app"; +import { account, invitation, member, organization, ssoProvider, usersTable } from "~/server/db/schema"; +import { db } from "~/server/db/db"; + +const app = createApp(); + +describe("auth SSO sign-in security", () => { + beforeEach(async () => { + await db.delete(member); + await db.delete(account); + await db.delete(invitation); + await db.delete(ssoProvider); + await db.delete(organization); + await db.delete(usersTable); + }); + + test("rejects malicious callback URL", async () => { + const response = await app.request("/api/auth/sign-in/sso", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + providerId: "missing-provider", + callbackURL: "https://evil.example", + }), + }); + + expect(response.status).toBe(400); + + const body = await response.json(); + expect(body.code).toContain("CALLBACKURL"); + expect(body.message).toContain("callbackURL"); + }); + + test("rejects malicious error callback URL", async () => { + const response = await app.request("/api/auth/sign-in/sso", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + providerId: "missing-provider", + callbackURL: "/login", + errorCallbackURL: "https://evil.example", + }), + }); + + expect(response.status).toBe(400); + + const body = await response.json(); + expect(body.code).toContain("ERRORCALLBACKURL"); + expect(body.message).toContain("errorCallbackURL"); + }); + + test("rejects malicious new user callback URL", async () => { + const response = await app.request("/api/auth/sign-in/sso", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + providerId: "missing-provider", + callbackURL: "/login", + newUserCallbackURL: "https://evil.example", + }), + }); + + expect(response.status).toBe(400); + + const body = await response.json(); + expect(body.code).toContain("NEWUSERCALLBACKURL"); + expect(body.message).toContain("newUserCallbackURL"); + }); + + test("allows relative callback URL to continue normal flow", async () => { + const response = await app.request("/api/auth/sign-in/sso", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + providerId: "missing-provider", + callbackURL: "/login", + }), + }); + + expect(response.status).toBe(404); + + const body = await response.json(); + expect(body.code).toBe("NO_PROVIDER_FOUND_FOR_THE_ISSUER"); + }); +}); diff --git a/app/server/modules/auth/auth.controller.ts b/app/server/modules/auth/auth.controller.ts index 8d0eb717..b3f9c675 100644 --- a/app/server/modules/auth/auth.controller.ts +++ b/app/server/modules/auth/auth.controller.ts @@ -1,13 +1,152 @@ import { Hono } from "hono"; -import { type GetStatusDto, getStatusDto, getUserDeletionImpactDto, type UserDeletionImpactDto } from "./auth.dto"; +import { validator } from "hono-openapi"; +import { + type GetStatusDto, + getStatusDto, + getUserDeletionImpactDto, + type UserDeletionImpactDto, + getPublicSsoProvidersDto, + type PublicSsoProvidersDto, + getSsoSettingsDto, + type SsoSettingsDto, + getAdminUsersDto, + type AdminUsersDto, + deleteSsoProviderDto, + deleteSsoInvitationDto, + updateSsoProviderAutoLinkingBody, + updateSsoProviderAutoLinkingDto, + deleteUserAccountDto, +} from "./auth.dto"; import { authService } from "./auth.service"; import { requireAdmin, requireAuth } from "./auth.middleware"; +import { auth } from "~/server/lib/auth"; export const authController = new Hono() .get("/status", getStatusDto, async (c) => { const hasUsers = await authService.hasUsers(); return c.json({ hasUsers }); }) + .get("/sso-providers", getPublicSsoProvidersDto, async (c) => { + const providers = await authService.getPublicSsoProviders(); + return c.json(providers); + }) + .get("/sso-settings", requireAuth, requireAdmin, getSsoSettingsDto, async (c) => { + const headers = c.req.raw.headers; + const activeOrganizationId = c.get("organizationId"); + + if (!activeOrganizationId) { + return c.json({ providers: [], invitations: [] }); + } + + const [providersData, invitationsData, autoLinkingSettings] = await Promise.all([ + auth.api.listSSOProviders({ headers }), + auth.api.listInvitations({ headers, query: { organizationId: activeOrganizationId } }), + authService.getSsoProviderAutoLinkingSettings(activeOrganizationId), + ]); + + return c.json({ + providers: providersData.providers.map((provider) => ({ + providerId: provider.providerId, + type: provider.type, + issuer: provider.issuer, + domain: provider.domain, + autoLinkMatchingEmails: autoLinkingSettings[provider.providerId] ?? false, + organizationId: provider.organizationId, + })), + invitations: invitationsData.map((invitation) => ({ + id: invitation.id, + email: invitation.email, + role: invitation.role, + status: invitation.status, + expiresAt: invitation.expiresAt.toISOString(), + })), + }); + }) + .delete("/sso-providers/:providerId", requireAuth, requireAdmin, deleteSsoProviderDto, async (c) => { + const providerId = c.req.param("providerId"); + const organizationId = c.get("organizationId"); + + const deleted = await authService.deleteSsoProvider(providerId, organizationId); + + if (!deleted) { + return c.json({ message: "Provider not found" }, 404); + } + + return c.json({ success: true }); + }) + .patch( + "/sso-providers/:providerId/auto-linking", + requireAuth, + requireAdmin, + updateSsoProviderAutoLinkingDto, + validator("json", updateSsoProviderAutoLinkingBody), + async (c) => { + const providerId = c.req.param("providerId"); + const organizationId = c.get("organizationId"); + const { enabled } = c.req.valid("json"); + + const updated = await authService.updateSsoProviderAutoLinking(providerId, organizationId, enabled); + + if (!updated) { + return c.json({ message: "Provider not found" }, 404); + } + + return c.json({ success: true }); + }, + ) + .delete("/sso-invitations/:invitationId", requireAuth, requireAdmin, deleteSsoInvitationDto, async (c) => { + const invitationId = c.req.param("invitationId"); + const organizationId = c.get("organizationId"); + + const invitation = await authService.getSsoInvitationById(invitationId); + if (!invitation || invitation.organizationId !== organizationId) { + return c.json({ message: "Invitation not found" }, 404); + } + + await authService.deleteSsoInvitation(invitationId); + + return c.json({ success: true }); + }) + .get("/admin-users", requireAuth, requireAdmin, getAdminUsersDto, async (c) => { + const headers = c.req.raw.headers; + + const usersData = await auth.api.listUsers({ + headers, + query: { limit: 100 }, + }); + + const userIds = usersData.users.map((u) => u.id); + const accountsByUser = await authService.getUserAccounts(userIds); + + return c.json({ + users: usersData.users.map((adminUser) => ({ + id: adminUser.id, + name: adminUser.name, + email: adminUser.email, + role: adminUser.role ?? "user", + banned: Boolean(adminUser.banned), + accounts: accountsByUser[adminUser.id] ?? [], + })), + total: usersData.total, + }); + }) + .delete("/admin-users/:userId/accounts/:accountId", requireAuth, requireAdmin, deleteUserAccountDto, async (c) => { + const userId = c.req.param("userId"); + const accountId = c.req.param("accountId"); + const organizationId = c.get("organizationId"); + + const result = await authService.deleteUserAccount(userId, accountId, organizationId); + + if (result.forbidden) { + return c.json({ message: "User is not a member of this organization" }, 403); + } + + if (result.lastAccount) { + return c.json({ message: "Cannot delete the last account of a user" }, 409); + } + + return c.json({ success: true }); + }) .get("/deletion-impact/:userId", requireAuth, requireAdmin, getUserDeletionImpactDto, async (c) => { const userId = c.req.param("userId"); const impact = await authService.getUserDeletionImpact(userId); diff --git a/app/server/modules/auth/auth.dto.ts b/app/server/modules/auth/auth.dto.ts index 4a795017..de2f274e 100644 --- a/app/server/modules/auth/auth.dto.ts +++ b/app/server/modules/auth/auth.dto.ts @@ -5,6 +5,102 @@ const statusResponseSchema = type({ hasUsers: "boolean", }); +export const publicSsoProvidersDto = type({ + providers: type({ + providerId: "string", + organizationSlug: "string", + }) + .onUndeclaredKey("delete") + .array(), +}); + +export type PublicSsoProvidersDto = typeof publicSsoProvidersDto.infer; + +export const getPublicSsoProvidersDto = describeRoute({ + description: "Get public SSO providers for the instance", + operationId: "getPublicSsoProviders", + tags: ["Auth"], + responses: { + 200: { + description: "List of public SSO providers", + content: { + "application/json": { + schema: resolver(publicSsoProvidersDto), + }, + }, + }, + }, +}); + +export const ssoSettingsResponse = type({ + providers: type({ + providerId: "string", + type: "string", + issuer: "string", + domain: "string", + autoLinkMatchingEmails: "boolean", + organizationId: "string | null", + }).array(), + invitations: type({ + id: "string", + email: "string", + role: "string", + status: "string", + expiresAt: "string", + }).array(), +}); + +export type SsoSettingsDto = typeof ssoSettingsResponse.infer; + +export const getSsoSettingsDto = describeRoute({ + description: "Get SSO providers and invitations for the active organization", + operationId: "getSsoSettings", + tags: ["Auth"], + responses: { + 200: { + description: "SSO settings for the active organization", + content: { + "application/json": { + schema: resolver(ssoSettingsResponse), + }, + }, + }, + }, +}); + +export const adminUsersResponse = type({ + users: type({ + id: "string", + name: "string | null", + email: "string", + role: "string", + banned: "boolean", + accounts: type({ + id: "string", + providerId: "string", + }).array(), + }).array(), + total: "number", +}); + +export type AdminUsersDto = typeof adminUsersResponse.infer; + +export const getAdminUsersDto = describeRoute({ + description: "List admin users for settings management", + operationId: "getAdminUsers", + tags: ["Auth"], + responses: { + 200: { + description: "List of users with roles and status", + content: { + "application/json": { + schema: resolver(adminUsersResponse), + }, + }, + }, + }, +}); + export const getStatusDto = describeRoute({ description: "Get authentication system status", operationId: "getStatus", @@ -52,3 +148,72 @@ export const getUserDeletionImpactDto = describeRoute({ }, }, }); + +export const deleteSsoProviderDto = describeRoute({ + description: "Delete an SSO provider", + operationId: "deleteSsoProvider", + tags: ["Auth"], + responses: { + 200: { + description: "SSO provider deleted successfully", + }, + 404: { + description: "Provider not found", + }, + 403: { + description: "Forbidden", + }, + }, +}); + +export const deleteSsoInvitationDto = describeRoute({ + description: "Delete an SSO invitation", + operationId: "deleteSsoInvitation", + tags: ["Auth"], + responses: { + 200: { + description: "SSO invitation deleted successfully", + }, + 403: { + description: "Forbidden", + }, + }, +}); + +export const deleteUserAccountDto = describeRoute({ + description: "Delete an account linked to a user", + operationId: "deleteUserAccount", + tags: ["Auth"], + responses: { + 200: { + description: "Account deleted successfully", + }, + 403: { + description: "Forbidden", + }, + 409: { + description: "Cannot delete the last account", + }, + }, +}); + +export const updateSsoProviderAutoLinkingBody = type({ + enabled: "boolean", +}); + +export const updateSsoProviderAutoLinkingDto = describeRoute({ + description: "Update whether SSO sign-in can auto-link existing accounts by email", + operationId: "updateSsoProviderAutoLinking", + tags: ["Auth"], + responses: { + 200: { + description: "SSO provider auto-linking setting updated successfully", + }, + 403: { + description: "Forbidden", + }, + 404: { + description: "Provider not found", + }, + }, +}); diff --git a/app/server/modules/auth/auth.middleware.ts b/app/server/modules/auth/auth.middleware.ts index feb606cd..1311f031 100644 --- a/app/server/modules/auth/auth.middleware.ts +++ b/app/server/modules/auth/auth.middleware.ts @@ -7,6 +7,7 @@ declare module "hono" { interface ContextVariableMap { user: { id: string; + email: string; username: string; hasDownloadedResticPassword: boolean; role?: string | null | undefined; diff --git a/app/server/modules/auth/auth.service.ts b/app/server/modules/auth/auth.service.ts index cf1bcc22..6e32d024 100644 --- a/app/server/modules/auth/auth.service.ts +++ b/app/server/modules/auth/auth.service.ts @@ -6,23 +6,42 @@ import { volumesTable, repositoriesTable, backupSchedulesTable, + ssoProvider, + account, + invitation, } from "../../db/schema"; import { eq, ne, and, count, inArray } from "drizzle-orm"; import type { UserDeletionImpactDto } from "./auth.dto"; +import { isReservedSsoProviderId } from "~/server/lib/auth/utils/sso-provider-id"; export class AuthService { /** * Check if any users exist in the system */ - async hasUsers(): Promise { + async hasUsers() { const [user] = await db.select({ id: usersTable.id }).from(usersTable).limit(1); return !!user; } + /** + * Get public SSO providers for the instance + */ + async getPublicSsoProviders() { + const providers = await db + .select({ + providerId: ssoProvider.providerId, + organizationSlug: organization.slug, + }) + .from(ssoProvider) + .innerJoin(organization, eq(ssoProvider.organizationId, organization.id)); + + return { providers }; + } + /** * Get the impact of deleting a user */ - async getUserDeletionImpact(userId: string): Promise { + async getUserDeletionImpact(userId: string) { const userMemberships = await db.query.member.findMany({ where: { AND: [{ userId: userId }, { role: "owner" }], @@ -77,7 +96,7 @@ export class AuthService { /** * Cleanup organizations where the user was the sole owner */ - async cleanupUserOrganizations(userId: string): Promise { + async cleanupUserOrganizations(userId: string) { const impact = await this.getUserDeletionImpact(userId); const orgIds = impact.organizations.map((o) => o.id); @@ -85,6 +104,137 @@ export class AuthService { await db.delete(organization).where(inArray(organization.id, orgIds)); } } + + /** + * Delete an SSO provider and its associated accounts + */ + async deleteSsoProvider(providerId: string, organizationId: string) { + return db.transaction(async (tx) => { + const provider = await tx.query.ssoProvider.findFirst({ + where: { AND: [{ providerId }, { organizationId }] }, + columns: { id: true, providerId: true }, + }); + + if (!provider) { + return false; + } + + if (isReservedSsoProviderId(provider.providerId)) { + await tx.delete(ssoProvider).where(eq(ssoProvider.id, provider.id)); + return true; + } + + await tx.delete(account).where(eq(account.providerId, provider.providerId)); + await tx.delete(ssoProvider).where(eq(ssoProvider.id, provider.id)); + + return true; + }); + } + + /** + * Get per-provider auto-linking setting for an organization + */ + async getSsoProviderAutoLinkingSettings(organizationId: string) { + const providers = await db.query.ssoProvider.findMany({ + columns: { providerId: true, autoLinkMatchingEmails: true }, + where: { organizationId }, + }); + + return Object.fromEntries(providers.map((provider) => [provider.providerId, provider.autoLinkMatchingEmails])); + } + + /** + * Update per-provider auto-linking setting + */ + async updateSsoProviderAutoLinking(providerId: string, organizationId: string, enabled: boolean) { + const existingProvider = await db.query.ssoProvider.findFirst({ + where: { AND: [{ providerId }, { organizationId }] }, + columns: { id: true }, + }); + + if (!existingProvider) { + return false; + } + + await db + .update(ssoProvider) + .set({ autoLinkMatchingEmails: enabled }) + .where(and(eq(ssoProvider.providerId, providerId), eq(ssoProvider.organizationId, organizationId))); + + return true; + } + + /** + * Get an SSO invitation by ID + */ + async getSsoInvitationById(invitationId: string) { + return db.query.invitation.findFirst({ + where: { id: invitationId }, + columns: { id: true, organizationId: true }, + }); + } + + /** + * Delete an invitation + */ + async deleteSsoInvitation(invitationId: string) { + await db.delete(invitation).where(eq(invitation.id, invitationId)); + } + + /** + * Check if a user is a member of an organization + */ + async getUserMembership(userId: string, organizationId: string) { + return db.query.member.findFirst({ + where: { AND: [{ userId }, { organizationId }] }, + columns: { id: true }, + }); + } + + /** + * Fetch accounts for a list of users, keyed by userId + */ + async getUserAccounts(userIds: string[]) { + if (userIds.length === 0) return {}; + + const accounts = await db.query.account.findMany({ + where: { userId: { in: userIds } }, + columns: { id: true, providerId: true, userId: true }, + }); + + const grouped: Record = {}; + for (const row of accounts) { + if (!grouped[row.userId]) { + grouped[row.userId] = []; + } + grouped[row.userId].push({ id: row.id, providerId: row.providerId }); + } + return grouped; + } + + /** + * Delete a single account for a user, refusing if it is the last one + */ + async deleteUserAccount(userId: string, accountId: string, organizationId: string) { + const membership = await this.getUserMembership(userId, organizationId); + if (!membership) { + return { lastAccount: false, forbidden: true }; + } + + return db.transaction(async (tx) => { + const userAccounts = await tx.query.account.findMany({ + where: { userId }, + columns: { id: true }, + }); + + if (userAccounts.length <= 1) { + return { lastAccount: true, forbidden: false }; + } + + await tx.delete(account).where(and(eq(account.id, accountId), eq(account.userId, userId))); + return { lastAccount: false, forbidden: false }; + }); + } } export const authService = new AuthService(); diff --git a/bun.lock b/bun.lock index 8d9af7e5..f41da0f0 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "zerobyte", "dependencies": { + "@better-auth/sso": "^1.4.18", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", @@ -15,6 +16,7 @@ "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-progress": "^1.1.8", @@ -114,6 +116,8 @@ "@ark/util": ["@ark/util@0.56.0", "", {}, "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA=="], + "@authenio/xml-encryption": ["@authenio/xml-encryption@2.0.2", "", { "dependencies": { "@xmldom/xmldom": "^0.8.6", "escape-html": "^1.0.3", "xpath": "0.0.32" } }, "sha512-cTlrKttbrRHEw3W+0/I609A2Matj5JQaRvfLtEIGZvlN0RaPi+3ANsMeqAyCAVlH/lUIW2tmtBlSMni74lcXeg=="], + "@azure-rest/core-client": ["@azure-rest/core-client@2.5.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A=="], "@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], @@ -122,7 +126,7 @@ "@azure/core-client": ["@azure/core-client@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "tslib": "^2.6.2" } }, "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w=="], - "@azure/core-http-compat": ["@azure/core-http-compat@2.3.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-client": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0" } }, "sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g=="], + "@azure/core-http-compat": ["@azure/core-http-compat@2.3.2", "", { "dependencies": { "@azure/abort-controller": "^2.1.2" }, "peerDependencies": { "@azure/core-client": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0" } }, "sha512-Tf6ltdKzOJEgxZeWLCjMxrxbodB/ZeCbzzA1A2qHbhzAjzjHoBVSUeSl/baT/oHAxhc4qdqVaDKnc2+iE932gw=="], "@azure/core-lro": ["@azure/core-lro@2.7.2", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-util": "^1.2.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2" } }, "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw=="], @@ -142,11 +146,11 @@ "@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="], - "@azure/msal-browser": ["@azure/msal-browser@4.28.1", "", { "dependencies": { "@azure/msal-common": "15.14.1" } }, "sha512-al2u2fTchbClq3L4C1NlqLm+vwKfhYCPtZN2LR/9xJVaQ4Mnrwf5vANvuyPSJHcGvw50UBmhuVmYUAhTEetTpA=="], + "@azure/msal-browser": ["@azure/msal-browser@4.29.0", "", { "dependencies": { "@azure/msal-common": "15.15.0" } }, "sha512-/f3eHkSNUTl6DLQHm+bKecjBKcRQxbd/XLx8lvSYp8Nl/HRyPuIPOijt9Dt0sH50/SxOwQ62RnFCmFlGK+bR/w=="], - "@azure/msal-common": ["@azure/msal-common@15.14.1", "", {}, "sha512-IkzF7Pywt6QKTS0kwdCv/XV8x8JXknZDvSjj/IccooxnP373T5jaadO3FnOrbWo3S0UqkfIDyZNTaQ/oAgRdXw=="], + "@azure/msal-common": ["@azure/msal-common@15.15.0", "", {}, "sha512-/n+bN0AKlVa+AOcETkJSKj38+bvFs78BaP4rNtv3MJCmPH0YrHiskMRe74OhyZ5DZjGISlFyxqvf9/4QVEi2tw=="], - "@azure/msal-node": ["@azure/msal-node@3.8.6", "", { "dependencies": { "@azure/msal-common": "15.14.1", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-XTmhdItcBckcVVTy65Xp+42xG4LX5GK+9AqAsXPXk4IqUNv+LyQo5TMwNjuFYBfAB2GTG9iSQGk+QLc03vhf3w=="], + "@azure/msal-node": ["@azure/msal-node@3.8.8", "", { "dependencies": { "@azure/msal-common": "15.15.0", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-+f1VrJH1iI517t4zgmuhqORja0bL6LDQXfBqkjuMmfTYXTQQnh1EvwwxO3UbKLT05N0obF72SRHFrC1RBDv5Gg=="], "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], @@ -212,6 +216,8 @@ "@better-auth/core": ["@better-auth/core@1.4.19", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.3.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.8", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-uADLHG1jc5BnEJi7f6ijUN5DmPPRSj++7m/G19z3UqA3MVCo4Y4t1MMa4IIxLCqGDFv22drdfxescgW+HnIowA=="], + "@better-auth/sso": ["@better-auth/sso@1.4.19", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "fast-xml-parser": "^5.2.5", "jose": "^6.1.0", "samlify": "^2.10.1", "zod": "^4.3.5" }, "peerDependencies": { "better-auth": "1.4.19", "better-call": "1.1.8" } }, "sha512-4OVPii6DQgIRm/DqO74Q4tZHx0LlNHXif3qrW7/iDzRO507h/AVAoJ9/U2fGycsPxUtPh1BT3PGHfX09sbF9Bw=="], + "@better-auth/telemetry": ["@better-auth/telemetry@1.4.19", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.19" } }, "sha512-ApGNS7olCTtDpKF8Ow3Z+jvFAirOj7c4RyFUpu8axklh3mH57ndpfUAUjhgA8UVoaaH/mnm/Tl884BlqiewLyw=="], "@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], @@ -234,57 +240,57 @@ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], "@faker-js/faker": ["@faker-js/faker@10.3.0", "", {}, "sha512-It0Sne6P3szg7JIi6CgKbvTZoMjxBZhcv91ZrqrNuaZQfB5WoqYYbzCUOq89YR+VY8juY9M1vDWmDDa2TzfXCw=="], @@ -596,6 +602,8 @@ "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="], + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], @@ -606,6 +614,8 @@ "@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="], + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="], + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], @@ -656,55 +666,55 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="], + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="], + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], "@scalar/core": ["@scalar/core@0.3.42", "", { "dependencies": { "@scalar/types": "0.6.7" } }, "sha512-RbyooMuG4oQEOhiA/tC+++bkIK1zeYGNxrTzSAgTrTzVlbFKPzw72fs4gX9/eHDo7qVc9FsymIW6qVpWbySzNg=="], @@ -876,15 +886,19 @@ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.2", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg=="], + "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.3", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-91fp6CAAJSRtH5ja95T1FHSKa8aPW9/Zw6cta81jlZTUw/+Vq8jM/AfF/14h2b71wwR84JUTW/3Y8QPhDAawFA=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="], + "@xmldom/is-dom-node": ["@xmldom/is-dom-node@1.0.1", "", {}, "sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q=="], + + "@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="], + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], - "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], @@ -908,6 +922,8 @@ "arktype": ["arktype@2.1.29", "", { "dependencies": { "@ark/schema": "0.56.0", "@ark/util": "0.56.0", "arkregex": "0.0.5" } }, "sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ=="], + "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], + "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], "babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="], @@ -918,7 +934,7 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="], "better-auth": ["better-auth@1.4.19", "", { "dependencies": { "@better-auth/core": "1.4.19", "@better-auth/telemetry": "1.4.19", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-3RlZJcA0+NH25wYD85vpIGwW9oSTuEmLIaGbT8zg41w/Pa2hVWHKedjoUHHJtnzkBXzDb+CShkLnSw7IThDdqQ=="], @@ -944,7 +960,9 @@ "c12": ["c12@3.3.3", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q=="], - "caniuse-lite": ["caniuse-lite@1.0.30001767", "", {}, "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ=="], + "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001774", "", {}, "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -978,7 +996,7 @@ "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], - "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], + "confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="], "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], @@ -1076,13 +1094,13 @@ "dotenv-expand": ["dotenv-expand@12.0.3", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA=="], - "drizzle-kit": ["drizzle-kit@1.0.0-beta.15-859cf75", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-Y36s1XQGVb1PgU3aRNgufp1K3D2VkIifu8kv4Ubsmxi+Dq+N7KMklnpp7Knu/XC4FZi2MHPPG3v3o097r0/TcQ=="], + "drizzle-kit": ["drizzle-kit@1.0.0-beta.9-e89174b", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "tsx": "^4.20.6" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-Xrw3k8E2CbSZr+kqe3k5W4oxd2fbEyczjKtyGIkAq0x9Wqpa/VtAT6Mkh83sIzqG4OSN7lOoUafsDxSE/AR7RA=="], "drizzle-orm": ["drizzle-orm@1.0.0-beta.9-e89174b", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@effect/sql": "^0.48.5", "@effect/sql-pg": "^0.49.7", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@sqlitecloud/drivers": ">=1.0.653", "@tidbcloud/serverless": "*", "@tursodatabase/database": ">=0.2.1", "@tursodatabase/database-common": ">=0.2.1", "@tursodatabase/database-wasm": ">=0.2.1", "@types/better-sqlite3": "*", "@types/mssql": "^9.1.4", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=9.3.0", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "mssql": "^11.0.1", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@effect/sql", "@effect/sql-pg", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@sqlitecloud/drivers", "@tidbcloud/serverless", "@tursodatabase/database", "@tursodatabase/database-common", "@tursodatabase/database-wasm", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-B5KR/qYMZ0JMOurK+0xi1ObpOQcgrjaC9wHUiU2eTJjLemuh2CoQIw6yur68NxZG6Xcd0b9qghUNC/78/bEfbg=="], "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], - "electron-to-chromium": ["electron-to-chromium@1.5.286", "", {}, "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A=="], + "electron-to-chromium": ["electron-to-chromium@1.5.302", "", {}, "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg=="], "encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="], @@ -1092,10 +1110,12 @@ "es-toolkit": ["es-toolkit@1.44.0", "", {}, "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="], - "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], @@ -1118,6 +1138,8 @@ "fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="], + "fast-xml-parser": ["fast-xml-parser@5.3.7", "", { "dependencies": { "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-JzVLro9NQv92pOM/jTCR6mHlJh2FGwtomH8ZQjhFj/R29P2Fnj38OgPJVtcvYw6SuKClhgYuwUZf5b3rd8u2mA=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], @@ -1132,7 +1154,7 @@ "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], - "get-tsconfig": ["get-tsconfig@4.13.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-EoY1N2xCn44xU6750Sx7OjOIT59FkmstNc3X6y5xpz7D5cBtZRe/3pSlTkDJgqsOk3WwZPkWfonhhUJfttQo3w=="], + "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], @@ -1142,7 +1164,7 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "h3": ["h3@2.0.1-rc.11", "", { "dependencies": { "rou3": "^0.7.12", "srvx": "^0.10.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-2myzjCqy32c1As9TjZW9fNZXtLqNedjFSrdFy2AjFBQQ3LzrnGoDdFDYfC0tV2e4vcyfJ2Sfo/F6NQhO2Ly/Mw=="], + "h3": ["h3@2.0.1-rc.14", "", { "dependencies": { "rou3": "^0.7.12", "srvx": "^0.11.2" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"], "bin": { "h3": "bin/h3.mjs" } }, "sha512-163qbGmTr/9rqQRNuqMqtgXnOUAkE4KTdauiC9y0E5iG1I65kte9NyfWvZw5RTDMt6eY+DtyoNzrQ9wA2BfvGQ=="], "h3-v2": ["h3@2.0.1-rc.14", "", { "dependencies": { "rou3": "^0.7.12", "srvx": "^0.11.2" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"], "bin": { "h3": "bin/h3.mjs" } }, "sha512-163qbGmTr/9rqQRNuqMqtgXnOUAkE4KTdauiC9y0E5iG1I65kte9NyfWvZw5RTDMt6eY+DtyoNzrQ9wA2BfvGQ=="], @@ -1206,7 +1228,7 @@ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], + "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], "isbot": ["isbot@5.1.35", "", {}, "sha512-waFfC72ZNfwLLuJ2iLaoVaqcNo+CAaLR7xCpAn0Y5WfGzkNHv7ZN39Vbi1y+kb+Zs46XHOX3tZNExroFUPX+Kg=="], @@ -1316,7 +1338,7 @@ "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], - "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], @@ -1428,13 +1450,17 @@ "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], + "node-forge": ["node-forge@1.3.3", "", {}, "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg=="], + "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + "node-rsa": ["node-rsa@1.1.1", "", { "dependencies": { "asn1": "^0.2.4" } }, "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw=="], + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], - "nypm": ["nypm@0.6.4", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw=="], + "nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], "ofetch": ["ofetch@2.0.0-alpha.3", "", {}, "sha512-zpYTCs2byOuft65vI3z43Dd6iSdFbOZZLb9/d21aCpx2rGastVU9dOCv0lu4ykc1Ur1anAYjDi3SUvR0vq50JA=="], @@ -1454,6 +1480,8 @@ "oxlint-tsgolint": ["oxlint-tsgolint@0.15.0", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.15.0", "@oxlint-tsgolint/darwin-x64": "0.15.0", "@oxlint-tsgolint/linux-arm64": "0.15.0", "@oxlint-tsgolint/linux-x64": "0.15.0", "@oxlint-tsgolint/win32-arm64": "0.15.0", "@oxlint-tsgolint/win32-x64": "0.15.0" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-iwvFmhKQVZzVTFygUVI4t2S/VKEm+Mqkw3jQRJwfDuTcUYI5LCIYzdO5Dbuv4mFOkXZCcXaRRh0m+uydB5xdqw=="], + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], @@ -1546,7 +1574,7 @@ "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], - "rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="], + "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], @@ -1556,6 +1584,8 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "samlify": ["samlify@2.10.2", "", { "dependencies": { "@authenio/xml-encryption": "^2.0.2", "@xmldom/xmldom": "^0.8.6", "camelcase": "^6.2.0", "node-forge": "^1.3.0", "node-rsa": "^1.1.1", "pako": "^1.0.10", "uuid": "^8.3.2", "xml": "^1.0.1", "xml-crypto": "^6.1.2", "xml-escape": "^1.1.0", "xpath": "^0.0.32" } }, "sha512-y5s1cHwclqwP8h7K2Wj9SfP1q+1S9+jrs5OAegYTLAiuFi7nDvuKqbiXLmUTvYPMpzHcX94wTY2+D604jgTKvA=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], @@ -1590,6 +1620,8 @@ "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + "strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], @@ -1604,7 +1636,7 @@ "tarn": ["tarn@3.0.2", "", {}, "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="], - "tedious": ["tedious@18.6.2", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.1", "@types/node": ">=18", "bl": "^6.0.11", "iconv-lite": "^0.6.3", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg=="], + "tedious": ["tedious@19.2.1", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.5", "@types/node": ">=18", "bl": "^6.1.4", "iconv-lite": "^0.7.0", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-pk1Q16Yl62iocuQB+RWbg6rFUFkIyzqOFQ6NfysCltRvQqKwfurgj8v/f2X+CKvDhSL4IJ0cCOfCHDg9PWEEYA=="], "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], @@ -1634,13 +1666,13 @@ "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], - "type-fest": ["type-fest@5.4.3", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-AXSAQJu79WGc79/3e9/CR77I/KQgeY1AhNvcShIH4PTcGYyC4xv6H4R4AUOwkPS5799KlVDAu8zExeCrkGquiA=="], + "type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], - "undici": ["undici@7.20.0", "", {}, "sha512-MJZrkjyd7DeC+uPZh+5/YaMDxFiiEEaDgbUSVMXayofAkDWF1088CDo+2RPg7B1BuS1qf1vgNE7xqwPxE0DuSQ=="], + "undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="], "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], @@ -1686,7 +1718,7 @@ "vite-tsconfig-paths": ["vite-tsconfig-paths@6.1.1", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg=="], - "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], + "vitefu": ["vitefu@1.1.2", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw=="], "wait-for-expect": ["wait-for-expect@4.0.0", "", {}, "sha512-mcH2HYUUHhdFGHVJkgwkBxRihZO4VSuPyh6xhYHz7LEnYkcaLbTAEEsTpYiFw4UY45XdTZYYIaquuMucw9wWMw=="], @@ -1708,8 +1740,16 @@ "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], + "xml": ["xml@1.0.1", "", {}, "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw=="], + + "xml-crypto": ["xml-crypto@6.1.2", "", { "dependencies": { "@xmldom/is-dom-node": "^1.0.1", "@xmldom/xmldom": "^0.8.10", "xpath": "^0.0.33" } }, "sha512-leBOVQdVi8FvPJrMYoum7Ici9qyxfE4kVi+AkpUoYCSXaQF4IlBm1cneTK9oAxR61LpYxTx7lNcsnBIeRpGW2w=="], + + "xml-escape": ["xml-escape@1.1.0", "", {}, "sha512-B/T4sDK8Z6aUh/qNr7mjKAwwncIljFuUP+DO/D5hloYFj+90O88z8Wf7oSucZTHxBAsC1/CTP4rtx/x1Uf72Mg=="], + "xmlbuilder2": ["xmlbuilder2@4.0.3", "", { "dependencies": { "@oozcitak/dom": "^2.0.2", "@oozcitak/infra": "^2.0.2", "@oozcitak/util": "^10.0.0", "js-yaml": "^4.1.1" } }, "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA=="], + "xpath": ["xpath@0.0.32", "", {}, "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], @@ -1726,8 +1766,6 @@ "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@happy-dom/global-registrator/@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="], - "@hey-api/shared/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -1738,6 +1776,8 @@ "@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-progress/@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="], @@ -1750,7 +1790,7 @@ "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@reduxjs/toolkit/immer": ["immer@11.1.3", "", {}, "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q=="], + "@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="], "@scalar/types/nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], @@ -1780,41 +1820,29 @@ "@tanstack/start-plugin-core/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@types/mssql/@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="], - - "@types/readable-stream/@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="], - - "@types/ws/@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="], - "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "bun-types/@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="], - - "c12/dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], - "cheerio/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], "cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - "dotenv-cli/dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], - "dotenv-expand/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], "encoding-sniffer/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "h3-v2/srvx": ["srvx@0.11.4", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-m/2p87bqWZ94xpRN06qNBwh0xq/D0dXajnvPDSHFqrTogxuTWYNP1UHz6Cf+oY7D+NPLY35TJAp4ESIKn0WArQ=="], + "h3/srvx": ["srvx@0.11.7", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-p9qj9wkv/MqG1VoJpOsqXv1QcaVcYRk7ifsC6i3TEwDXFyugdhJN4J3KzQPZq2IJJ2ZCt7ASOB++85pEK38jRw=="], - "happy-dom/@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="], - - "jsonwebtoken/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "h3-v2/srvx": ["srvx@0.11.7", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-p9qj9wkv/MqG1VoJpOsqXv1QcaVcYRk7ifsC6i3TEwDXFyugdhJN4J3KzQPZq2IJJ2ZCt7ASOB++85pEK38jRw=="], "libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], "mssql/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], - "nypm/citty": ["citty@0.2.0", "", {}, "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA=="], + "mssql/tedious": ["tedious@18.6.2", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.1", "@types/node": ">=18", "bl": "^6.0.11", "iconv-lite": "^0.6.3", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg=="], + + "nypm/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], @@ -1824,29 +1852,15 @@ "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "tedious/@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="], - - "tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "xml-crypto/xpath": ["xpath@0.0.33", "", {}, "sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA=="], + "@azure/identity/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], - "@happy-dom/global-registrator/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "@tanstack/router-plugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - "@types/mssql/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "@types/readable-stream/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "@types/ws/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "happy-dom/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "tedious/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "mssql/tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "@tanstack/router-plugin/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], } diff --git a/docker-compose.yml b/docker-compose.yml index edcadd0d..eada0b84 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -61,13 +61,25 @@ services: - DISABLE_RATE_LIMITING=true - APP_SECRET=94bad4678ce84a60b9789bd2114a6bf780aeb38df426f7352c941c66e25d5c2b - BASE_URL=http://localhost:4096 + - TRUSTED_ORIGINS=http://dex:5557,http://localhost:5557 devices: - /dev/fuse:/dev/fuse cap_add: - SYS_ADMIN ports: - "4096:4096" + depends_on: + - dex volumes: - /etc/localtime:/etc/localtime:ro - ./playwright/data:/var/lib/zerobyte/data - ./playwright/temp:/test-data + + dex: + image: ghcr.io/dexidp/dex:latest + restart: unless-stopped + ports: + - "5557:5557" + volumes: + - ./playwright/dex-config.yaml:/etc/dex/cfg.yaml:ro + command: dex serve /etc/dex/cfg.yaml diff --git a/e2e/0002-backup-restore.spec.ts b/e2e/0002-backup-restore.spec.ts index 0106d7ef..60ec9ca2 100644 --- a/e2e/0002-backup-restore.spec.ts +++ b/e2e/0002-backup-restore.spec.ts @@ -1,85 +1,115 @@ -import { expect, test } from "@playwright/test"; -import path from "node:path"; import fs from "node:fs"; +import { randomUUID } from "node:crypto"; +import path from "node:path"; +import { expect, test, type Page, type TestInfo } from "@playwright/test"; import { gotoAndWaitForAppReady } from "./helpers/page"; -test.describe.configure({ mode: "serial" }); +const testDataPath = path.join(process.cwd(), "playwright", "temp"); -test.beforeAll(() => { - const testDataPath = path.join(process.cwd(), "playwright", "temp"); - if (fs.existsSync(testDataPath)) { - for (const file of fs.readdirSync(testDataPath)) { - fs.rmSync(path.join(testDataPath, file), { recursive: true, force: true }); - } - } else { - fs.mkdirSync(testDataPath, { recursive: true }); - } -}); +type ScenarioNames = { + volumeName: string; + repositoryName: string; + backupName: string; +}; -test("can backup & restore a file", async ({ page }) => { - await gotoAndWaitForAppReady(page, "/"); - await expect(page).toHaveURL("/volumes"); +function getRunId(testInfo: TestInfo) { + return `${testInfo.parallelIndex}-${testInfo.retry}-${randomUUID().slice(0, 8)}`; +} - // 0. Create a test file in /test-data - const testDataPath = path.join(process.cwd(), "playwright", "temp"); - const filePath = path.join(testDataPath, "test.json"); +function getScenarioNames(runId: string): ScenarioNames { + return { + volumeName: `Volume-${runId}`, + repositoryName: `Repo-${runId}`, + backupName: `Backup-${runId}`, + }; +} + +function prepareTestFile(runId: string): string { + const runPath = path.join(testDataPath, runId); + fs.mkdirSync(runPath, { recursive: true }); + + const filePath = path.join(runPath, "test.json"); fs.writeFileSync(filePath, JSON.stringify({ data: "test file" })); - // 1. Create a local volume on /test-data + return filePath; +} + +async function createBackupScenario(page: Page, names: ScenarioNames) { await page.getByRole("button", { name: "Create Volume" }).click(); - await page.getByRole("textbox", { name: "Name" }).fill("Test Volume"); + await page.getByRole("textbox", { name: "Name" }).fill(names.volumeName); await page.getByRole("button", { name: "test-data" }).click(); await page.getByRole("button", { name: "Create Volume" }).click(); await expect(page.getByText("Volume created successfully")).toBeVisible(); - // 2. Create a local repository on the default location await page.getByRole("link", { name: "Repositories" }).click(); await page.getByRole("button", { name: "Create repository" }).click(); - await page.getByRole("textbox", { name: "Name" }).fill("Test Repo"); + await page.getByRole("textbox", { name: "Name" }).fill(names.repositoryName); await page.getByRole("combobox", { name: "Backend" }).click(); await page.getByRole("option", { name: "Local" }).click(); await page.getByRole("button", { name: "Create repository" }).click(); - await expect(page.getByText("Repository created successfully")).toBeVisible(); + await expect(page.getByText("Repository created successfully")).toBeVisible({ timeout: 30000 }); - // 3. Create a backup schedule await page.getByRole("link", { name: "Backups" }).click(); - await page.getByRole("button", { name: "Create a backup job" }).click(); + const createBackupButton = page.getByRole("button", { name: "Create a backup job" }).first(); + if (await createBackupButton.isVisible()) { + await createBackupButton.click(); + } else { + await page.getByRole("link", { name: "Create a backup job" }).first().click(); + } await page.getByRole("combobox").filter({ hasText: "Choose a volume to backup" }).click(); - await page.getByRole("option", { name: "Test Volume" }).click(); - await page.getByRole("textbox", { name: "Backup name" }).fill("Test Backup"); + await page.getByRole("option", { name: names.volumeName }).click(); + await page.getByRole("textbox", { name: "Backup name" }).fill(names.backupName); await page.getByRole("combobox").filter({ hasText: "Select a repository" }).click(); - await page.getByRole("option", { name: "Test Repo" }).click(); + await page.getByRole("option", { name: names.repositoryName }).click(); await page.getByRole("combobox").filter({ hasText: "Select frequency" }).click(); await page.getByRole("option", { name: "Daily" }).click(); await page.getByRole("textbox", { name: "Execution time" }).fill("00:00"); await page.getByRole("button", { name: "Create" }).click(); await expect(page.getByText("Backup job created successfully")).toBeVisible(); +} + +test("can backup & restore a file", async ({ page }, testInfo) => { + const runId = getRunId(testInfo); + const names = getScenarioNames(runId); + const filePath = prepareTestFile(runId); + + await gotoAndWaitForAppReady(page, "/"); + await expect(page).toHaveURL("/volumes"); + + await createBackupScenario(page, names); - // 4. Runs that schedule once await page.getByRole("button", { name: "Backup now" }).click(); await expect(page.getByText("Backup started successfully")).toBeVisible(); await expect(page.getByText("✓ Success")).toBeVisible({ timeout: 30000 }); - // 5. Modify the json file after the backup fs.writeFileSync(filePath, JSON.stringify({ data: "modified file" })); - // 6. Restores the file from backup - await page.getByRole("button", { name: /20 B/ }).click(); + await page + .getByRole("button", { name: /\d+ B$/ }) + .first() + .click(); await page.getByRole("link", { name: "Restore" }).click(); await expect(page).toHaveURL(/\/restore/); await page.getByRole("button", { name: "Restore All" }).click(); await expect(page.getByText("Restore completed")).toBeVisible({ timeout: 30000 }); - // 7. Ensures that the file is back to its previous state const restoredContent = fs.readFileSync(filePath, "utf8"); expect(JSON.parse(restoredContent)).toEqual({ data: "test file" }); }); -test("deleting a volume cascades and removes its backup schedule", async ({ page }) => { - await gotoAndWaitForAppReady(page, "/backups"); - await page.getByText("Test Backup", { exact: true }).first().click(); +test("deleting a volume cascades and removes its backup schedule", async ({ page }, testInfo) => { + const runId = getRunId(testInfo); + const names = getScenarioNames(runId); - const volumeLink = page.locator("main").getByRole("link", { name: "Test Volume", exact: true }).first(); + await gotoAndWaitForAppReady(page, "/"); + await expect(page).toHaveURL("/volumes"); + + await createBackupScenario(page, names); + + await gotoAndWaitForAppReady(page, "/backups"); + await page.getByText(names.backupName, { exact: true }).first().click(); + + const volumeLink = page.locator("main").getByRole("link", { name: names.volumeName, exact: true }).first(); await expect(volumeLink).toBeVisible(); await volumeLink.click(); await expect(page).toHaveURL(/\/volumes\/[^/?#]+/); @@ -95,5 +125,5 @@ test("deleting a volume cascades and removes its backup schedule", async ({ page await expect(page.getByText("Volume deleted successfully")).toBeVisible(); await gotoAndWaitForAppReady(page, "/backups"); - await expect(page.getByText("Test Backup", { exact: true })).toHaveCount(0); + await expect(page.getByText(names.backupName, { exact: true })).toHaveCount(0); }); diff --git a/e2e/0003-oidc.spec.ts b/e2e/0003-oidc.spec.ts new file mode 100644 index 00000000..65fa16a1 --- /dev/null +++ b/e2e/0003-oidc.spec.ts @@ -0,0 +1,167 @@ +import { expect, test, type Browser, type Page } from "@playwright/test"; +import { gotoAndWaitForAppReady, waitForAppReady } from "./helpers/page"; + +const dexOrigin = process.env.E2E_DEX_ORIGIN ?? "http://dex:5557"; +const issuer = `${dexOrigin}/dex`; +const discoveryEndpoint = `${issuer}/.well-known/openid-configuration`; +const appBaseUrl = `http://${process.env.SERVER_IP ?? "localhost"}:4096`; + +const providerIds = { + register: "test-oidc-register", + uninvited: "test-oidc-uninvited", + invited: "test-oidc-invited", + autoLink: "test-oidc", +} as const; + +const dexPassword = "password"; +const uninvitedUserEmail = "admin@example.com"; +const invitedUserEmail = "user@example.com"; +const autoLinkTargetEmail = "test@example.com"; +const autoLinkTargetUsername = "sso-link-target"; +const inviteOnlyMessage = + "Access is invite-only. Ask an organization admin to send you an invitation before signing in with SSO."; + +async function openSsoSettings(page: Page) { + await gotoAndWaitForAppReady(page, "/settings?tab=users"); + await expect(page.getByText("Single Sign-On")).toBeVisible(); +} + +async function registerOidcProvider(page: Page, providerId: string) { + await gotoAndWaitForAppReady(page, "/settings/sso/new"); + + await page.getByRole("textbox", { name: "Provider ID" }).fill(providerId); + await page.getByRole("textbox", { name: "Organization Domain" }).fill("example.com"); + await page.getByRole("textbox", { name: "Issuer URL" }).fill(issuer); + await page.getByRole("textbox", { name: "Discovery Endpoint" }).fill(discoveryEndpoint); + await page.getByRole("textbox", { name: "Client ID" }).fill("zerobyte-test"); + await page.getByRole("textbox", { name: "Client Secret" }).fill("test-secret-12345"); + await page.getByRole("button", { name: "Register Provider" }).click(); + + await expect(page.getByText("SSO provider registered successfully")).toBeVisible(); + await expect(page.getByRole("cell", { name: providerId, exact: true })).toBeVisible(); +} + +async function createPendingInvitation(page: Page, email: string) { + const response = await page.request.post("/api/auth/organization/invite-member", { + headers: { + Origin: appBaseUrl, + }, + data: { + email, + role: "member", + }, + }); + + if (!response.ok()) { + throw new Error(`Failed to invite ${email}: ${await response.text()}`); + } +} + +async function createAutoLinkTargetUser(page: Page, email: string, username: string) { + const response = await page.request.post("/api/auth/admin/create-user", { + headers: { + Origin: appBaseUrl, + }, + data: { + email, + password: dexPassword, + name: "SSO Link Target", + role: "user", + data: { + username, + hasDownloadedResticPassword: true, + }, + }, + }); + + if (!response.ok()) { + throw new Error(`Failed to create auto-link target user: ${await response.text()}`); + } +} + +async function setProviderAutoLinking(page: Page, providerId: string, enabled: boolean) { + await openSsoSettings(page); + const providerRow = page + .getByRole("row") + .filter({ has: page.getByRole("cell", { name: providerId, exact: true }) }) + .first(); + const autoLinkSwitch = providerRow.getByRole("switch"); + const expectedState = enabled ? "true" : "false"; + const currentState = await autoLinkSwitch.getAttribute("aria-checked"); + const expectedToast = enabled ? "Automatic account linking enabled" : "Automatic account linking disabled"; + + if (currentState !== expectedState) { + await autoLinkSwitch.click(); + await expect(page.getByText(expectedToast)).toBeVisible(); + } + + await expect(autoLinkSwitch).toHaveAttribute("aria-checked", expectedState); +} + +async function withOidcLoginAttempt( + browser: Browser, + providerId: string, + dexLogin: string, + assertions: (page: Page) => Promise, +) { + const context = await browser.newContext({ + storageState: { + cookies: [], + origins: [], + }, + }); + const page = await context.newPage(); + + try { + await gotoAndWaitForAppReady(page, `${appBaseUrl}/login`); + await page.getByRole("button", { name: `Log in with ${providerId}`, exact: true }).click(); + await page.waitForURL(/\/dex\/auth/, { timeout: 60000 }); + + await page.locator('input[name="login"]').fill(dexLogin); + await page.locator('input[name="password"]').fill(dexPassword); + await page.locator('button[type="submit"]').click(); + + await assertions(page); + } finally { + await context.close().catch(() => undefined); + } +} + +test("admin can register an OIDC provider", async ({ page }) => { + await registerOidcProvider(page, providerIds.register); +}); + +test("uninvited OIDC users are blocked", async ({ page, browser }) => { + await registerOidcProvider(page, providerIds.uninvited); + + await withOidcLoginAttempt(browser, providerIds.uninvited, uninvitedUserEmail, async (ssoPage) => { + await ssoPage.waitForURL(/\/login(\/error)?/, { timeout: 60000 }); + await waitForAppReady(ssoPage); + await expect(ssoPage.getByText(inviteOnlyMessage)).toBeVisible(); + }); +}); + +test("invited OIDC users can sign in", async ({ page, browser }) => { + await registerOidcProvider(page, providerIds.invited); + await createPendingInvitation(page, invitedUserEmail); + + await withOidcLoginAttempt(browser, providerIds.invited, invitedUserEmail, async (ssoPage) => { + await ssoPage.waitForURL(/\/volumes/, { timeout: 60000 }); + await waitForAppReady(ssoPage); + await expect(ssoPage).toHaveURL(/\/volumes/); + }); +}); + +test("auto-link setting can be enabled for an OIDC provider", async ({ page, browser }) => { + await registerOidcProvider(page, providerIds.autoLink); + await createAutoLinkTargetUser(page, autoLinkTargetEmail, autoLinkTargetUsername); + await createPendingInvitation(page, autoLinkTargetEmail); + + await setProviderAutoLinking(page, providerIds.autoLink, true); + + await withOidcLoginAttempt(browser, providerIds.autoLink, autoLinkTargetEmail, async (ssoPage) => { + await ssoPage.waitForURL(/\/volumes/, { timeout: 60000 }); + await waitForAppReady(ssoPage); + await expect(ssoPage).toHaveURL(/\/volumes/); + }); +}); diff --git a/package.json b/package.json index 1035b00e..5811c29b 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "tsc": "tsc --noEmit", "start:dev": "docker compose down && docker compose up --build zerobyte-dev", "start:prod": "docker compose down && docker compose up --build zerobyte-prod", - "start:e2e": "docker compose down && docker compose up --build zerobyte-e2e", + "start:e2e": "docker compose down && rm -f playwright/data/zerobyte.db playwright/data/cache.db playwright/data/restic.pass playwright/.auth/user.json playwright/restic.pass && docker compose up --build zerobyte-e2e", "start:distroless": "docker compose down && docker compose up --build zerobyte-distroless", "gen:api-client": "dotenv -e .env.local -- openapi-ts", "gen:migrations": "dotenv -e .env.local -- drizzle-kit generate", @@ -26,6 +26,7 @@ "test:codegen": "playwright codegen localhost:4096" }, "dependencies": { + "@better-auth/sso": "^1.4.18", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", @@ -36,6 +37,7 @@ "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-progress": "^1.1.8", diff --git a/playwright.config.ts b/playwright.config.ts index 0acbfcc9..4fe4fcf0 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -5,8 +5,8 @@ export default defineConfig({ testDir: "./e2e", fullyParallel: true, forbidOnly: !!process.env.CI, + timeout: 90000, retries: 0, - workers: process.env.CI ? 1 : undefined, reporter: "html", use: { baseURL: `http://${process.env.SERVER_IP}:4096`, @@ -17,12 +17,16 @@ export default defineConfig({ { name: "setup", testMatch: /.*\.setup\.ts/, + workers: 1, }, { name: "chromium", use: { ...devices["Desktop Chrome"], storageState: "playwright/.auth/user.json", + launchOptions: { + args: ["--host-rules=MAP dex 127.0.0.1"], + }, }, dependencies: ["setup"], }, diff --git a/playwright/dex-config.yaml b/playwright/dex-config.yaml new file mode 100644 index 00000000..15bed466 --- /dev/null +++ b/playwright/dex-config.yaml @@ -0,0 +1,50 @@ +issuer: http://dex:5557/dex + +storage: + type: sqlite3 + config: + file: /tmp/dex.db + +web: + http: 0.0.0.0:5557 + +enablePasswordDB: true + +staticPasswords: + - email: "admin@example.com" + hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" + username: "admin" + - email: "user@example.com" + hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" + username: "user" + - email: "test@example.com" + hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" + username: "test" + - email: "test@test.com" + hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" + username: "test-local" + +staticClients: + - id: zerobyte-test + name: Zerobyte Test + redirectURIs: + - "http://localhost:3000/api/auth/sso/callback/credential" + - "http://localhost:3000/api/auth/sso/callback/dex" + - "http://localhost:4096/api/auth/sso/callback/test-oidc" + - "http://localhost:4096/api/auth/sso/callback/test-oidc-register" + - "http://localhost:4096/api/auth/sso/callback/test-oidc-uninvited" + - "http://localhost:4096/api/auth/sso/callback/test-oidc-invited" + - "http://localhost:4096/api/auth/sso/callback/test-oidc-autolink" + secret: test-secret-12345 + +oauth2: + skipApprovalScreen: true + +logger: + level: debug + format: json + +# Issuer URL: http://dex:5557/dex +# Discovery URL: http://dex:5557/dex/.well-known/openid-configuration +# Client ID: zerobyte-test +# Client Secret: test-secret-12345