diff --git a/.github/workflows/test_next_server.yml b/.github/workflows/test_next_server.yml new file mode 100644 index 00000000..892566d6 --- /dev/null +++ b/.github/workflows/test_next_server.yml @@ -0,0 +1,45 @@ +name: Test Next Server + +on: + pull_request: + paths: + - "www/**" + push: + branches: + - main + paths: + - "www/**" + +jobs: + test-next-server: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: ./www + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 8 + + - name: Setup Node.js cache + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + cache-dependency-path: './www/pnpm-lock.yaml' + + - name: Install dependencies + run: pnpm install + + - name: Run tests + run: pnpm test \ No newline at end of file diff --git a/.gitignore b/.gitignore index 29d56f25..f3249991 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ server/test.sqlite CLAUDE.local.md www/.env.development www/.env.production +.playwright-mcp diff --git a/compose.yml b/compose.yml index 492c7b8c..acbfd3b5 100644 --- a/compose.yml +++ b/compose.yml @@ -6,6 +6,7 @@ services: - 1250:1250 volumes: - ./server/:/app/ + - /app/.venv env_file: - ./server/.env environment: @@ -16,6 +17,7 @@ services: context: server volumes: - ./server/:/app/ + - /app/.venv env_file: - ./server/.env environment: @@ -26,6 +28,7 @@ services: context: server volumes: - ./server/:/app/ + - /app/.venv env_file: - ./server/.env environment: diff --git a/server/reflector/views/rooms.py b/server/reflector/views/rooms.py index 40e81aeb..cc00f3c0 100644 --- a/server/reflector/views/rooms.py +++ b/server/reflector/views/rooms.py @@ -197,6 +197,7 @@ async def rooms_create_meeting( end_date = current_time + timedelta(hours=8) whereby_meeting = await create_meeting("", end_date=end_date, room=room) + await upload_logo(whereby_meeting["roomName"], "./images/logo.png") # Now try to save to database diff --git a/server/reflector/views/transcripts.py b/server/reflector/views/transcripts.py index 3f32a9bd..9acfcbf8 100644 --- a/server/reflector/views/transcripts.py +++ b/server/reflector/views/transcripts.py @@ -27,6 +27,7 @@ from reflector.db.search import ( from reflector.db.transcripts import ( SourceKind, TranscriptParticipant, + TranscriptStatus, TranscriptTopic, transcripts_controller, ) @@ -63,7 +64,7 @@ class GetTranscriptMinimal(BaseModel): id: str user_id: str | None name: str - status: str + status: TranscriptStatus locked: bool duration: float title: str | None diff --git a/server/runserver.sh b/server/runserver.sh index a4fb6869..9cccaacb 100755 --- a/server/runserver.sh +++ b/server/runserver.sh @@ -2,7 +2,7 @@ if [ "${ENTRYPOINT}" = "server" ]; then uv run alembic upgrade head - uv run -m reflector.app + uv run uvicorn reflector.app:app --host 0.0.0.0 --port 1250 elif [ "${ENTRYPOINT}" = "worker" ]; then uv run celery -A reflector.worker.app worker --loglevel=info elif [ "${ENTRYPOINT}" = "beat" ]; then diff --git a/www/app/(app)/AuthWrapper.tsx b/www/app/(app)/AuthWrapper.tsx new file mode 100644 index 00000000..57038b7b --- /dev/null +++ b/www/app/(app)/AuthWrapper.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { Flex, Spinner } from "@chakra-ui/react"; +import { useAuth } from "../lib/AuthProvider"; +import { useLoginRequiredPages } from "../lib/useLoginRequiredPages"; + +export default function AuthWrapper({ + children, +}: { + children: React.ReactNode; +}) { + const auth = useAuth(); + const redirectPath = useLoginRequiredPages(); + const redirectHappens = !!redirectPath; + + if (auth.status === "loading" || redirectHappens) { + return ( + + + + ); + } + + return <>{children}; +} diff --git a/www/app/(app)/browse/_components/FilterSidebar.tsx b/www/app/(app)/browse/_components/FilterSidebar.tsx index b2abe481..6eef61b8 100644 --- a/www/app/(app)/browse/_components/FilterSidebar.tsx +++ b/www/app/(app)/browse/_components/FilterSidebar.tsx @@ -1,7 +1,10 @@ import React from "react"; import { Box, Stack, Link, Heading } from "@chakra-ui/react"; import NextLink from "next/link"; -import { Room, SourceKind } from "../../../api"; +import type { components } from "../../../reflector-api"; + +type Room = components["schemas"]["Room"]; +type SourceKind = components["schemas"]["SourceKind"]; interface FilterSidebarProps { rooms: Room[]; @@ -72,7 +75,7 @@ export default function FilterSidebar({ key={room.id} as={NextLink} href="#" - onClick={() => onFilterChange("room", room.id)} + onClick={() => onFilterChange("room" as SourceKind, room.id)} color={ selectedSourceKind === "room" && selectedRoomId === room.id ? "blue.500" diff --git a/www/app/(app)/browse/_components/TranscriptCards.tsx b/www/app/(app)/browse/_components/TranscriptCards.tsx index b67e71e7..8dbc3568 100644 --- a/www/app/(app)/browse/_components/TranscriptCards.tsx +++ b/www/app/(app)/browse/_components/TranscriptCards.tsx @@ -18,7 +18,10 @@ import { highlightMatches, generateTextFragment, } from "../../../lib/textHighlight"; -import { SearchResult } from "../../../api"; +import type { components } from "../../../reflector-api"; + +type SearchResult = components["schemas"]["SearchResult"]; +type SourceKind = components["schemas"]["SourceKind"]; interface TranscriptCardsProps { results: SearchResult[]; @@ -120,7 +123,7 @@ function TranscriptCard({ : "N/A"; const formattedDate = formatLocalDate(result.created_at); const source = - result.source_kind === "room" + result.source_kind === ("room" as SourceKind) ? result.room_name || result.room_id : result.source_kind; diff --git a/www/app/(app)/browse/page.tsx b/www/app/(app)/browse/page.tsx index e7522e14..8523650e 100644 --- a/www/app/(app)/browse/page.tsx +++ b/www/app/(app)/browse/page.tsx @@ -19,37 +19,33 @@ import { parseAsStringLiteral, } from "nuqs"; import { LuX } from "react-icons/lu"; -import { useSearchTranscripts } from "../transcripts/useSearchTranscripts"; -import useSessionUser from "../../lib/useSessionUser"; -import { Room, SourceKind, SearchResult, $SourceKind } from "../../api"; -import useApi from "../../lib/useApi"; -import { useError } from "../../(errors)/errorContext"; +import type { components } from "../../reflector-api"; + +type Room = components["schemas"]["Room"]; +type SourceKind = components["schemas"]["SourceKind"]; +type SearchResult = components["schemas"]["SearchResult"]; +import { + useRoomsList, + useTranscriptsSearch, + useTranscriptDelete, + useTranscriptProcess, +} from "../../lib/apiHooks"; import FilterSidebar from "./_components/FilterSidebar"; import Pagination, { FIRST_PAGE, PaginationPage, parsePaginationPage, totalPages as getTotalPages, + paginationPageTo0Based, } from "./_components/Pagination"; import TranscriptCards from "./_components/TranscriptCards"; import DeleteTranscriptDialog from "./_components/DeleteTranscriptDialog"; import { formatLocalDate } from "../../lib/time"; import { RECORD_A_MEETING_URL } from "../../api/urls"; +import { useUserName } from "../../lib/useUserName"; const SEARCH_FORM_QUERY_INPUT_NAME = "query" as const; -const usePrefetchRooms = (setRooms: (rooms: Room[]) => void): void => { - const { setError } = useError(); - const api = useApi(); - useEffect(() => { - if (!api) return; - api - .v1RoomsList({ page: 1 }) - .then((rooms) => setRooms(rooms.items)) - .catch((err) => setError(err, "There was an error fetching the rooms")); - }, [api, setError]); -}; - const SearchForm: React.FC<{ setPage: (page: PaginationPage) => void; sourceKind: SourceKind | null; @@ -69,7 +65,6 @@ const SearchForm: React.FC<{ searchQuery, setSearchQuery, }) => { - // to keep the search input controllable + more fine grained control (urlSearchQuery is updated on submits) const [searchInputValue, setSearchInputValue] = useState(searchQuery || ""); const handleSearchQuerySubmit = async (d: FormData) => { await setSearchQuery((d.get(SEARCH_FORM_QUERY_INPUT_NAME) as string) || ""); @@ -163,7 +158,6 @@ const UnderSearchFormFilterIndicators: React.FC<{ p="1px" onClick={() => { setSourceKind(null); - // TODO questionable setRoomId(null); }} _hover={{ bg: "blue.200" }} @@ -209,7 +203,11 @@ export default function TranscriptBrowser() { const [urlSourceKind, setUrlSourceKind] = useQueryState( "source", - parseAsStringLiteral($SourceKind.enum).withOptions({ + parseAsStringLiteral([ + "room", + "live", + "file", + ] as const satisfies SourceKind[]).withOptions({ shallow: false, }), ); @@ -229,46 +227,40 @@ export default function TranscriptBrowser() { useEffect(() => { const maybePage = parsePaginationPage(urlPage); if ("error" in maybePage) { - setPage(FIRST_PAGE).then(() => { - /*may be called n times we dont care*/ - }); + setPage(FIRST_PAGE).then(() => {}); return; } _setSafePage(maybePage.value); }, [urlPage]); - const [rooms, setRooms] = useState([]); - const pageSize = 20; + const { - results, - totalCount: totalResults, - isLoading, - reload, - } = useSearchTranscripts( - urlSearchQuery, - { - roomIds: urlRoomId ? [urlRoomId] : null, - sourceKind: urlSourceKind, - }, - { - pageSize, - page, - }, - ); + data: searchData, + isLoading: searchLoading, + refetch: reloadSearch, + } = useTranscriptsSearch(urlSearchQuery, { + limit: pageSize, + offset: paginationPageTo0Based(page) * pageSize, + room_id: urlRoomId || undefined, + source_kind: urlSourceKind || undefined, + }); + + const results = searchData?.results || []; + const totalResults = searchData?.total || 0; + + // Fetch rooms + const { data: roomsData } = useRoomsList(1); + const rooms = roomsData?.items || []; const totalPages = getTotalPages(totalResults, pageSize); - const userName = useSessionUser().name; + const userName = useUserName(); const [deletionLoading, setDeletionLoading] = useState(false); - const api = useApi(); - const { setError } = useError(); const cancelRef = React.useRef(null); const [transcriptToDeleteId, setTranscriptToDeleteId] = React.useState(); - usePrefetchRooms(setRooms); - const handleFilterTranscripts = ( sourceKind: SourceKind | null, roomId: string, @@ -280,44 +272,37 @@ export default function TranscriptBrowser() { const onCloseDeletion = () => setTranscriptToDeleteId(undefined); + const deleteTranscript = useTranscriptDelete(); + const processTranscript = useTranscriptProcess(); + const confirmDeleteTranscript = (transcriptId: string) => { - if (!api || deletionLoading) return; + if (deletionLoading) return; setDeletionLoading(true); - api - .v1TranscriptDelete({ transcriptId }) - .then(() => { - setDeletionLoading(false); - onCloseDeletion(); - reload(); - }) - .catch((err) => { - setDeletionLoading(false); - setError(err, "There was an error deleting the transcript"); - }); + deleteTranscript.mutate( + { + params: { + path: { transcript_id: transcriptId }, + }, + }, + { + onSuccess: () => { + setDeletionLoading(false); + onCloseDeletion(); + reloadSearch(); + }, + onError: () => { + setDeletionLoading(false); + }, + }, + ); }; const handleProcessTranscript = (transcriptId: string) => { - if (!api) { - console.error("API not available on handleProcessTranscript"); - return; - } - api - .v1TranscriptProcess({ transcriptId }) - .then((result) => { - const status = - result && typeof result === "object" && "status" in result - ? (result as { status: string }).status - : undefined; - if (status === "already running") { - setError( - new Error("Processing is already running, please wait"), - "Processing is already running, please wait", - ); - } - }) - .catch((err) => { - setError(err, "There was an error processing the transcript"); - }); + processTranscript.mutate({ + params: { + path: { transcript_id: transcriptId }, + }, + }); }; const transcriptToDelete = results?.find( @@ -332,7 +317,7 @@ export default function TranscriptBrowser() { ? transcriptToDelete.room_name || transcriptToDelete.room_id : transcriptToDelete?.source_kind; - if (isLoading && results.length === 0) { + if (searchLoading && results.length === 0) { return ( {userName ? `${userName}'s Transcriptions` : "Your Transcriptions"}{" "} - {(isLoading || deletionLoading) && } + {(searchLoading || deletionLoading) && } @@ -403,12 +388,12 @@ export default function TranscriptBrowser() { - {!isLoading && results.length === 0 && ( + {!searchLoading && results.length === 0 && ( )} diff --git a/www/app/(app)/layout.tsx b/www/app/(app)/layout.tsx index 5760e19d..801be28f 100644 --- a/www/app/(app)/layout.tsx +++ b/www/app/(app)/layout.tsx @@ -2,9 +2,8 @@ import { Container, Flex, Link } from "@chakra-ui/react"; import { getConfig } from "../lib/edgeConfig"; import NextLink from "next/link"; import Image from "next/image"; -import About from "../(aboutAndPrivacy)/about"; -import Privacy from "../(aboutAndPrivacy)/privacy"; import UserInfo from "../(auth)/userInfo"; +import AuthWrapper from "./AuthWrapper"; import { RECORD_A_MEETING_URL } from "../api/urls"; export default async function AppLayout({ @@ -90,7 +89,7 @@ export default async function AppLayout({ - {children} + {children} ); } diff --git a/www/app/(app)/rooms/_components/RoomCards.tsx b/www/app/(app)/rooms/_components/RoomCards.tsx index 16748d90..8b22ad72 100644 --- a/www/app/(app)/rooms/_components/RoomCards.tsx +++ b/www/app/(app)/rooms/_components/RoomCards.tsx @@ -12,11 +12,13 @@ import { HStack, } from "@chakra-ui/react"; import { LuLink } from "react-icons/lu"; -import { RoomDetails } from "../../../api"; +import type { components } from "../../../reflector-api"; + +type Room = components["schemas"]["Room"]; import { RoomActionsMenu } from "./RoomActionsMenu"; interface RoomCardsProps { - rooms: RoomDetails[]; + rooms: Room[]; linkCopied: string; onCopyUrl: (roomName: string) => void; onEdit: (roomId: string, roomData: any) => void; diff --git a/www/app/(app)/rooms/_components/RoomList.tsx b/www/app/(app)/rooms/_components/RoomList.tsx index 73fe8a5c..218c890c 100644 --- a/www/app/(app)/rooms/_components/RoomList.tsx +++ b/www/app/(app)/rooms/_components/RoomList.tsx @@ -1,11 +1,13 @@ import { Box, Heading, Text, VStack } from "@chakra-ui/react"; -import { RoomDetails } from "../../../api"; +import type { components } from "../../../reflector-api"; + +type Room = components["schemas"]["Room"]; import { RoomTable } from "./RoomTable"; import { RoomCards } from "./RoomCards"; interface RoomListProps { title: string; - rooms: RoomDetails[]; + rooms: Room[]; linkCopied: string; onCopyUrl: (roomName: string) => void; onEdit: (roomId: string, roomData: any) => void; diff --git a/www/app/(app)/rooms/_components/RoomTable.tsx b/www/app/(app)/rooms/_components/RoomTable.tsx index 93d05b61..113eca7f 100644 --- a/www/app/(app)/rooms/_components/RoomTable.tsx +++ b/www/app/(app)/rooms/_components/RoomTable.tsx @@ -9,11 +9,13 @@ import { Spinner, } from "@chakra-ui/react"; import { LuLink } from "react-icons/lu"; -import { RoomDetails } from "../../../api"; +import type { components } from "../../../reflector-api"; + +type Room = components["schemas"]["Room"]; import { RoomActionsMenu } from "./RoomActionsMenu"; interface RoomTableProps { - rooms: RoomDetails[]; + rooms: Room[]; linkCopied: string; onCopyUrl: (roomName: string) => void; onEdit: (roomId: string, roomData: any) => void; diff --git a/www/app/(app)/rooms/page.tsx b/www/app/(app)/rooms/page.tsx index 33cfa6b3..8b1378df 100644 --- a/www/app/(app)/rooms/page.tsx +++ b/www/app/(app)/rooms/page.tsx @@ -15,13 +15,24 @@ import { createListCollection, useDisclosure, } from "@chakra-ui/react"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { LuEye, LuEyeOff } from "react-icons/lu"; -import useApi from "../../lib/useApi"; import useRoomList from "./useRoomList"; -import { ApiError, RoomDetails } from "../../api"; +import type { components } from "../../reflector-api"; +import { + useRoomCreate, + useRoomUpdate, + useRoomDelete, + useZulipStreams, + useZulipTopics, + useRoomGet, + useRoomTestWebhook, +} from "../../lib/apiHooks"; import { RoomList } from "./_components/RoomList"; import { PaginationPage } from "../browse/_components/Pagination"; +import { assertExists } from "../../lib/utils"; + +type Room = components["schemas"]["Room"]; interface SelectOption { label: string; @@ -76,66 +87,77 @@ export default function RoomsList() { const recordingTypeCollection = createListCollection({ items: recordingTypeOptions, }); - const [room, setRoom] = useState(roomInitialState); + const [roomInput, setRoomInput] = useState( + null, + ); const [isEditing, setIsEditing] = useState(false); - const [editRoomId, setEditRoomId] = useState(""); - const api = useApi(); - // TODO seems to be no setPage calls - const [page, setPage] = useState(1); - const { loading, response, refetch } = useRoomList(PaginationPage(page)); - const [streams, setStreams] = useState([]); - const [topics, setTopics] = useState([]); + const [editRoomId, setEditRoomId] = useState(null); + const { + loading, + response, + refetch, + error: roomListError, + } = useRoomList(PaginationPage(1)); const [nameError, setNameError] = useState(""); const [linkCopied, setLinkCopied] = useState(""); + const [selectedStreamId, setSelectedStreamId] = useState(null); const [testingWebhook, setTestingWebhook] = useState(false); const [webhookTestResult, setWebhookTestResult] = useState( null, ); const [showWebhookSecret, setShowWebhookSecret] = useState(false); - interface Stream { - stream_id: number; - name: string; - } - interface Topic { - name: string; - } + const createRoomMutation = useRoomCreate(); + const updateRoomMutation = useRoomUpdate(); + const deleteRoomMutation = useRoomDelete(); + const { data: streams = [] } = useZulipStreams(); + const { data: topics = [] } = useZulipTopics(selectedStreamId); + const { + data: detailedEditedRoom, + isLoading: isDetailedEditedRoomLoading, + error: detailedEditedRoomError, + } = useRoomGet(editRoomId); + + const error = roomListError || detailedEditedRoomError; + + // room being edited, as fetched from the server + const editedRoom: typeof roomInitialState | null = useMemo( + () => + detailedEditedRoom + ? { + name: detailedEditedRoom.name, + zulipAutoPost: detailedEditedRoom.zulip_auto_post, + zulipStream: detailedEditedRoom.zulip_stream, + zulipTopic: detailedEditedRoom.zulip_topic, + isLocked: detailedEditedRoom.is_locked, + roomMode: detailedEditedRoom.room_mode, + recordingType: detailedEditedRoom.recording_type, + recordingTrigger: detailedEditedRoom.recording_trigger, + isShared: detailedEditedRoom.is_shared, + webhookUrl: detailedEditedRoom.webhook_url || "", + webhookSecret: detailedEditedRoom.webhook_secret || "", + } + : null, + [detailedEditedRoom], + ); + + // a room input value or a last api room state + const room = roomInput || editedRoom || roomInitialState; + + const roomTestWebhookMutation = useRoomTestWebhook(); + + // Update selected stream ID when zulip stream changes useEffect(() => { - const fetchZulipStreams = async () => { - if (!api) return; - - try { - const response = await api.v1ZulipGetStreams(); - setStreams(response); - } catch (error) { - console.error("Error fetching Zulip streams:", error); + if (room.zulipStream && streams.length > 0) { + const selectedStream = streams.find((s) => s.name === room.zulipStream); + if (selectedStream !== undefined) { + setSelectedStreamId(selectedStream.stream_id); } - }; - - if (room.zulipAutoPost) { - fetchZulipStreams(); + } else { + setSelectedStreamId(null); } - }, [room.zulipAutoPost, !api]); - - useEffect(() => { - const fetchZulipTopics = async () => { - if (!api || !room.zulipStream) return; - try { - const selectedStream = streams.find((s) => s.name === room.zulipStream); - if (selectedStream) { - const response = await api.v1ZulipGetTopics({ - streamId: selectedStream.stream_id, - }); - setTopics(response); - } - } catch (error) { - console.error("Error fetching Zulip topics:", error); - } - }; - - fetchZulipTopics(); - }, [room.zulipStream, streams, api]); + }, [room.zulipStream, streams]); const streamOptions: SelectOption[] = streams.map((stream) => { return { label: stream.name, value: stream.name }; @@ -167,35 +189,42 @@ export default function RoomsList() { const handleCloseDialog = () => { setShowWebhookSecret(false); setWebhookTestResult(null); + setEditRoomId(null); onClose(); }; const handleTestWebhook = async () => { - if (!room.webhookUrl || !editRoomId) { + if (!room.webhookUrl) { setWebhookTestResult("Please enter a webhook URL first"); return; } + if (!editRoomId) { + console.error("No room ID to test webhook"); + return; + } setTestingWebhook(true); setWebhookTestResult(null); try { - const response = await api?.v1RoomsTestWebhook({ - roomId: editRoomId, + const response = await roomTestWebhookMutation.mutateAsync({ + params: { + path: { + room_id: editRoomId, + }, + }, }); - if (response?.success) { + if (response.success) { setWebhookTestResult( `✅ Webhook test successful! Status: ${response.status_code}`, ); } else { let errorMsg = `❌ Webhook test failed`; - if (response?.status_code) { - errorMsg += ` (Status: ${response.status_code})`; - } - if (response?.error) { + errorMsg += ` (Status: ${response.status_code})`; + if (response.error) { errorMsg += `: ${response.error}`; - } else if (response?.response_preview) { + } else if (response.response_preview) { // Try to parse and extract meaningful error from response // Specific to N8N at the moment, as there is no specification for that // We could just display as is, but decided here to dig a little bit more. @@ -249,27 +278,29 @@ export default function RoomsList() { }; if (isEditing) { - await api?.v1RoomsUpdate({ - roomId: editRoomId, - requestBody: roomData, + await updateRoomMutation.mutateAsync({ + params: { + path: { room_id: assertExists(editRoomId) }, + }, + body: roomData, }); } else { - await api?.v1RoomsCreate({ - requestBody: roomData, + await createRoomMutation.mutateAsync({ + body: roomData, }); } - setRoom(roomInitialState); + setRoomInput(null); setIsEditing(false); setEditRoomId(""); setNameError(""); refetch(); + onClose(); handleCloseDialog(); - } catch (err) { + } catch (err: any) { if ( - err instanceof ApiError && - err.status === 400 && - (err.body as any).detail == "Room name is not unique" + err?.status === 400 && + err?.body?.detail == "Room name is not unique" ) { setNameError( "This room name is already taken. Please choose a different name.", @@ -280,46 +311,11 @@ export default function RoomsList() { } }; - const handleEditRoom = async (roomId, roomData) => { + const handleEditRoom = async (roomId: string, roomData) => { // Reset states setShowWebhookSecret(false); setWebhookTestResult(null); - // Fetch full room details to get webhook fields - try { - const detailedRoom = await api?.v1RoomsGet({ roomId }); - if (detailedRoom) { - setRoom({ - name: detailedRoom.name, - zulipAutoPost: detailedRoom.zulip_auto_post, - zulipStream: detailedRoom.zulip_stream, - zulipTopic: detailedRoom.zulip_topic, - isLocked: detailedRoom.is_locked, - roomMode: detailedRoom.room_mode, - recordingType: detailedRoom.recording_type, - recordingTrigger: detailedRoom.recording_trigger, - isShared: detailedRoom.is_shared, - webhookUrl: detailedRoom.webhook_url || "", - webhookSecret: detailedRoom.webhook_secret || "", - }); - } - } catch (error) { - console.error("Failed to fetch room details, using list data:", error); - // Fallback to using the data from the list - setRoom({ - name: roomData.name, - zulipAutoPost: roomData.zulip_auto_post, - zulipStream: roomData.zulip_stream, - zulipTopic: roomData.zulip_topic, - isLocked: roomData.is_locked, - roomMode: roomData.room_mode, - recordingType: roomData.recording_type, - recordingTrigger: roomData.recording_trigger, - isShared: roomData.is_shared, - webhookUrl: roomData.webhook_url || "", - webhookSecret: roomData.webhook_secret || "", - }); - } setEditRoomId(roomId); setIsEditing(true); setNameError(""); @@ -328,8 +324,10 @@ export default function RoomsList() { const handleDeleteRoom = async (roomId: string) => { try { - await api?.v1RoomsDelete({ - roomId, + await deleteRoomMutation.mutateAsync({ + params: { + path: { room_id: roomId }, + }, }); refetch(); } catch (err) { @@ -346,15 +344,15 @@ export default function RoomsList() { .toLowerCase(); setNameError(""); } - setRoom({ + setRoomInput({ ...room, [name]: type === "checkbox" ? checked : value, }); }; - const myRooms: RoomDetails[] = + const myRooms: Room[] = response?.items.filter((roomData) => !roomData.is_shared) || []; - const sharedRooms: RoomDetails[] = + const sharedRooms: Room[] = response?.items.filter((roomData) => roomData.is_shared) || []; if (loading && !response) @@ -369,6 +367,9 @@ export default function RoomsList() { ); + if (roomListError) + return
{`${roomListError.name}: ${roomListError.message}`}
; + return ( { setIsEditing(false); - setRoom(roomInitialState); + setRoomInput(null); setNameError(""); setShowWebhookSecret(false); setWebhookTestResult(null); @@ -456,7 +457,7 @@ export default function RoomsList() { - setRoom({ ...room, roomMode: e.value[0] }) + setRoomInput({ ...room, roomMode: e.value[0] }) } collection={roomModeCollection} > @@ -486,7 +487,7 @@ export default function RoomsList() { - setRoom({ + setRoomInput({ ...room, recordingType: e.value[0], recordingTrigger: @@ -521,7 +522,7 @@ export default function RoomsList() { - setRoom({ ...room, recordingTrigger: e.value[0] }) + setRoomInput({ ...room, recordingTrigger: e.value[0] }) } collection={recordingTriggerCollection} disabled={room.recordingType !== "cloud"} @@ -576,7 +577,7 @@ export default function RoomsList() { - setRoom({ + setRoomInput({ ...room, zulipStream: e.value[0], zulipTopic: "", @@ -611,7 +612,7 @@ export default function RoomsList() { - setRoom({ ...room, zulipTopic: e.value[0] }) + setRoomInput({ ...room, zulipTopic: e.value[0] }) } collection={topicCollection} disabled={!room.zulipAutoPost} diff --git a/www/app/(app)/rooms/useRoomList.tsx b/www/app/(app)/rooms/useRoomList.tsx index c1021ade..e8d11250 100644 --- a/www/app/(app)/rooms/useRoomList.tsx +++ b/www/app/(app)/rooms/useRoomList.tsx @@ -1,48 +1,27 @@ -import { useEffect, useState } from "react"; -import { useError } from "../../(errors)/errorContext"; -import useApi from "../../lib/useApi"; -import { Page_RoomDetails_ } from "../../api"; +import { useRoomsList } from "../../lib/apiHooks"; +import type { components } from "../../reflector-api"; + +type Page_Room_ = components["schemas"]["Page_RoomDetails_"]; import { PaginationPage } from "../browse/_components/Pagination"; type RoomList = { - response: Page_RoomDetails_ | null; + response: Page_Room_ | null; loading: boolean; error: Error | null; refetch: () => void; }; -//always protected +// Wrapper to maintain backward compatibility const useRoomList = (page: PaginationPage): RoomList => { - const [response, setResponse] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setErrorState] = useState(null); - const { setError } = useError(); - const api = useApi(); - const [refetchCount, setRefetchCount] = useState(0); - - const refetch = () => { - setLoading(true); - setRefetchCount(refetchCount + 1); + const { data, isLoading, error, refetch } = useRoomsList(page); + return { + response: data || null, + loading: isLoading, + error: error + ? new Error(error.detail ? JSON.stringify(error.detail) : undefined) + : null, + refetch, }; - - useEffect(() => { - if (!api) return; - setLoading(true); - api - .v1RoomsList({ page }) - .then((response) => { - setResponse(response); - setLoading(false); - }) - .catch((err) => { - setResponse(null); - setLoading(false); - setError(err); - setErrorState(err); - }); - }, [!api, page, refetchCount]); - - return { response, loading, error, refetch }; }; export default useRoomList; diff --git a/www/app/(app)/transcripts/[transcriptId]/correct/page.tsx b/www/app/(app)/transcripts/[transcriptId]/correct/page.tsx index 9eff7b60..c885ca6e 100644 --- a/www/app/(app)/transcripts/[transcriptId]/correct/page.tsx +++ b/www/app/(app)/transcripts/[transcriptId]/correct/page.tsx @@ -6,9 +6,10 @@ import TopicPlayer from "./topicPlayer"; import useParticipants from "../../useParticipants"; import useTopicWithWords from "../../useTopicWithWords"; import ParticipantList from "./participantList"; -import { GetTranscriptTopic } from "../../../../api"; +import type { components } from "../../../../reflector-api"; +type GetTranscriptTopic = components["schemas"]["GetTranscriptTopic"]; import { SelectedText, selectedTextIsTimeSlice } from "./types"; -import useApi from "../../../../lib/useApi"; +import { useTranscriptUpdate } from "../../../../lib/apiHooks"; import useTranscript from "../../useTranscript"; import { useError } from "../../../../(errors)/errorContext"; import { useRouter } from "next/navigation"; @@ -23,7 +24,7 @@ export type TranscriptCorrect = { export default function TranscriptCorrect({ params: { transcriptId }, }: TranscriptCorrect) { - const api = useApi(); + const updateTranscriptMutation = useTranscriptUpdate(); const transcript = useTranscript(transcriptId); const stateCurrentTopic = useState(); const [currentTopic, _sct] = stateCurrentTopic; @@ -34,16 +35,21 @@ export default function TranscriptCorrect({ const { setError } = useError(); const router = useRouter(); - const markAsDone = () => { + const markAsDone = async () => { if (transcript.response && !transcript.response.reviewed) { - api - ?.v1TranscriptUpdate({ transcriptId, requestBody: { reviewed: true } }) - .then(() => { - router.push(`/transcripts/${transcriptId}`); - }) - .catch((e) => { - setError(e, "Error marking as done"); + try { + await updateTranscriptMutation.mutateAsync({ + params: { + path: { + transcript_id: transcriptId, + }, + }, + body: { reviewed: true }, }); + router.push(`/transcripts/${transcriptId}`); + } catch (e) { + setError(e as Error, "Error marking as done"); + } } }; diff --git a/www/app/(app)/transcripts/[transcriptId]/correct/participantList.tsx b/www/app/(app)/transcripts/[transcriptId]/correct/participantList.tsx index e9297c4b..7c60ea54 100644 --- a/www/app/(app)/transcripts/[transcriptId]/correct/participantList.tsx +++ b/www/app/(app)/transcripts/[transcriptId]/correct/participantList.tsx @@ -1,8 +1,15 @@ import { faArrowTurnDown } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { ChangeEvent, useEffect, useRef, useState } from "react"; -import { Participant } from "../../../../api"; -import useApi from "../../../../lib/useApi"; +import type { components } from "../../../../reflector-api"; +type Participant = components["schemas"]["Participant"]; +import { + useTranscriptSpeakerAssign, + useTranscriptSpeakerMerge, + useTranscriptParticipantUpdate, + useTranscriptParticipantCreate, + useTranscriptParticipantDelete, +} from "../../../../lib/apiHooks"; import { UseParticipants } from "../../useParticipants"; import { selectedTextIsSpeaker, selectedTextIsTimeSlice } from "./types"; import { useError } from "../../../../(errors)/errorContext"; @@ -30,9 +37,19 @@ const ParticipantList = ({ topicWithWords, stateSelectedText, }: ParticipantList) => { - const api = useApi(); const { setError } = useError(); - const [loading, setLoading] = useState(false); + const speakerAssignMutation = useTranscriptSpeakerAssign(); + const speakerMergeMutation = useTranscriptSpeakerMerge(); + const participantUpdateMutation = useTranscriptParticipantUpdate(); + const participantCreateMutation = useTranscriptParticipantCreate(); + const participantDeleteMutation = useTranscriptParticipantDelete(); + + const loading = + speakerAssignMutation.isPending || + speakerMergeMutation.isPending || + participantUpdateMutation.isPending || + participantCreateMutation.isPending || + participantDeleteMutation.isPending; const [participantInput, setParticipantInput] = useState(""); const inputRef = useRef(null); const [selectedText, setSelectedText] = stateSelectedText; @@ -103,7 +120,6 @@ const ParticipantList = ({ const onSuccess = () => { topicWithWords.refetch(); participants.refetch(); - setLoading(false); setAction(null); setSelectedText(undefined); setSelectedParticipant(undefined); @@ -120,11 +136,14 @@ const ParticipantList = ({ if (loading || participants.loading || topicWithWords.loading) return; if (!selectedTextIsTimeSlice(selectedText)) return; - setLoading(true); try { - await api?.v1TranscriptAssignSpeaker({ - transcriptId, - requestBody: { + await speakerAssignMutation.mutateAsync({ + params: { + path: { + transcript_id: transcriptId, + }, + }, + body: { participant: participant.id, timestamp_from: selectedText.start, timestamp_to: selectedText.end, @@ -132,8 +151,7 @@ const ParticipantList = ({ }); onSuccess(); } catch (error) { - setError(error, "There was an error assigning"); - setLoading(false); + setError(error as Error, "There was an error assigning"); throw error; } }; @@ -141,32 +159,38 @@ const ParticipantList = ({ const mergeSpeaker = (speakerFrom, participantTo: Participant) => async () => { if (loading || participants.loading || topicWithWords.loading) return; - setLoading(true); + if (participantTo.speaker) { try { - await api?.v1TranscriptMergeSpeaker({ - transcriptId, - requestBody: { + await speakerMergeMutation.mutateAsync({ + params: { + path: { + transcript_id: transcriptId, + }, + }, + body: { speaker_from: speakerFrom, speaker_to: participantTo.speaker, }, }); onSuccess(); } catch (error) { - setError(error, "There was an error merging"); - setLoading(false); + setError(error as Error, "There was an error merging"); } } else { try { - await api?.v1TranscriptUpdateParticipant({ - transcriptId, - participantId: participantTo.id, - requestBody: { speaker: speakerFrom }, + await participantUpdateMutation.mutateAsync({ + params: { + path: { + transcript_id: transcriptId, + participant_id: participantTo.id, + }, + }, + body: { speaker: speakerFrom }, }); onSuccess(); } catch (error) { - setError(error, "There was an error merging (update)"); - setLoading(false); + setError(error as Error, "There was an error merging (update)"); } } }; @@ -186,105 +210,106 @@ const ParticipantList = ({ (p) => p.speaker == selectedText, ); if (participant && participant.name !== participantInput) { - setLoading(true); - api - ?.v1TranscriptUpdateParticipant({ - transcriptId, - participantId: participant.id, - requestBody: { + try { + await participantUpdateMutation.mutateAsync({ + params: { + path: { + transcript_id: transcriptId, + participant_id: participant.id, + }, + }, + body: { name: participantInput, }, - }) - .then(() => { - participants.refetch(); - setLoading(false); - setAction(null); - }) - .catch((e) => { - setError(e, "There was an error renaming"); - setLoading(false); }); + participants.refetch(); + setAction(null); + } catch (e) { + setError(e as Error, "There was an error renaming"); + } } } else if ( action == "Create to rename" && selectedTextIsSpeaker(selectedText) ) { - setLoading(true); - api - ?.v1TranscriptAddParticipant({ - transcriptId, - requestBody: { + try { + await participantCreateMutation.mutateAsync({ + params: { + path: { + transcript_id: transcriptId, + }, + }, + body: { name: participantInput, speaker: selectedText, }, - }) - .then(() => { - participants.refetch(); - setParticipantInput(""); - setOneMatch(undefined); - setLoading(false); - }) - .catch((e) => { - setError(e, "There was an error creating"); - setLoading(false); }); + participants.refetch(); + setParticipantInput(""); + setOneMatch(undefined); + } catch (e) { + setError(e as Error, "There was an error creating"); + } } else if ( action == "Create and assign" && selectedTextIsTimeSlice(selectedText) ) { - setLoading(true); try { - const participant = await api?.v1TranscriptAddParticipant({ - transcriptId, - requestBody: { + const participant = await participantCreateMutation.mutateAsync({ + params: { + path: { + transcript_id: transcriptId, + }, + }, + body: { name: participantInput, }, }); - setLoading(false); assignTo(participant)().catch(() => { // error and loading are handled by assignTo catch participants.refetch(); }); } catch (error) { - setError(e, "There was an error creating"); - setLoading(false); + setError(error as Error, "There was an error creating"); } } else if (action == "Create") { - setLoading(true); - api - ?.v1TranscriptAddParticipant({ - transcriptId, - requestBody: { + try { + await participantCreateMutation.mutateAsync({ + params: { + path: { + transcript_id: transcriptId, + }, + }, + body: { name: participantInput, }, - }) - .then(() => { - participants.refetch(); - setParticipantInput(""); - setLoading(false); - inputRef.current?.focus(); - }) - .catch((e) => { - setError(e, "There was an error creating"); - setLoading(false); }); + participants.refetch(); + setParticipantInput(""); + inputRef.current?.focus(); + } catch (e) { + setError(e as Error, "There was an error creating"); + } } }; - const deleteParticipant = (participantId) => (e) => { + const deleteParticipant = (participantId) => async (e) => { e.stopPropagation(); if (loading || participants.loading || topicWithWords.loading) return; - setLoading(true); - api - ?.v1TranscriptDeleteParticipant({ transcriptId, participantId }) - .then(() => { - participants.refetch(); - setLoading(false); - }) - .catch((e) => { - setError(e, "There was an error deleting"); - setLoading(false); + + try { + await participantDeleteMutation.mutateAsync({ + params: { + path: { + transcript_id: transcriptId, + participant_id: participantId, + }, + }, }); + participants.refetch(); + } catch (e) { + setError(e as Error, "There was an error deleting"); + } }; const selectParticipant = (participant) => (e) => { diff --git a/www/app/(app)/transcripts/[transcriptId]/correct/topicHeader.tsx b/www/app/(app)/transcripts/[transcriptId]/correct/topicHeader.tsx index 1448de80..494d2929 100644 --- a/www/app/(app)/transcripts/[transcriptId]/correct/topicHeader.tsx +++ b/www/app/(app)/transcripts/[transcriptId]/correct/topicHeader.tsx @@ -1,6 +1,7 @@ import useTopics from "../../useTopics"; import { Dispatch, SetStateAction, useEffect } from "react"; -import { GetTranscriptTopic } from "../../../../api"; +import type { components } from "../../../../reflector-api"; +type GetTranscriptTopic = components["schemas"]["GetTranscriptTopic"]; import { BoxProps, Box, diff --git a/www/app/(app)/transcripts/[transcriptId]/finalSummary.tsx b/www/app/(app)/transcripts/[transcriptId]/finalSummary.tsx index 4ce4a9e1..b1f61d43 100644 --- a/www/app/(app)/transcripts/[transcriptId]/finalSummary.tsx +++ b/www/app/(app)/transcripts/[transcriptId]/finalSummary.tsx @@ -2,12 +2,10 @@ import { useEffect, useRef, useState } from "react"; import React from "react"; import Markdown from "react-markdown"; import "../../../styles/markdown.css"; -import { - GetTranscript, - GetTranscriptTopic, - UpdateTranscript, -} from "../../../api"; -import useApi from "../../../lib/useApi"; +import type { components } from "../../../reflector-api"; +type GetTranscript = components["schemas"]["GetTranscript"]; +type GetTranscriptTopic = components["schemas"]["GetTranscriptTopic"]; +import { useTranscriptUpdate } from "../../../lib/apiHooks"; import { Flex, Heading, @@ -33,9 +31,8 @@ export default function FinalSummary(props: FinalSummaryProps) { const [preEditSummary, setPreEditSummary] = useState(""); const [editedSummary, setEditedSummary] = useState(""); - const api = useApi(); - const { setError } = useError(); + const updateTranscriptMutation = useTranscriptUpdate(); useEffect(() => { setEditedSummary(props.transcriptResponse?.long_summary || ""); @@ -47,12 +44,15 @@ export default function FinalSummary(props: FinalSummaryProps) { const updateSummary = async (newSummary: string, transcriptId: string) => { try { - const requestBody: UpdateTranscript = { - long_summary: newSummary, - }; - const updatedTranscript = await api?.v1TranscriptUpdate({ - transcriptId, - requestBody, + const updatedTranscript = await updateTranscriptMutation.mutateAsync({ + params: { + path: { + transcript_id: transcriptId, + }, + }, + body: { + long_summary: newSummary, + }, }); if (props.onUpdate) { props.onUpdate(newSummary); @@ -60,7 +60,7 @@ export default function FinalSummary(props: FinalSummaryProps) { console.log("Updated long summary:", updatedTranscript); } catch (err) { console.error("Failed to update long summary:", err); - setError(err, "Failed to update long summary."); + setError(err as Error, "Failed to update long summary."); } }; @@ -114,7 +114,12 @@ export default function FinalSummary(props: FinalSummaryProps) { - + )} {!isEditMode && ( diff --git a/www/app/(app)/transcripts/[transcriptId]/page.tsx b/www/app/(app)/transcripts/[transcriptId]/page.tsx index 0a2dba47..ce48e951 100644 --- a/www/app/(app)/transcripts/[transcriptId]/page.tsx +++ b/www/app/(app)/transcripts/[transcriptId]/page.tsx @@ -86,7 +86,7 @@ export default function TranscriptDetails(details: TranscriptDetails) { useActiveTopic={useActiveTopic} waveform={waveform.waveform} media={mp3.media} - mediaDuration={transcript.response.duration} + mediaDuration={transcript.response?.duration || null} /> ) : !mp3.loading && (waveform.error || mp3.error) ? ( @@ -116,7 +116,7 @@ export default function TranscriptDetails(details: TranscriptDetails) { { transcript.reload(); diff --git a/www/app/(app)/transcripts/[transcriptId]/upload/page.tsx b/www/app/(app)/transcripts/[transcriptId]/upload/page.tsx index 3a13052e..567272ff 100644 --- a/www/app/(app)/transcripts/[transcriptId]/upload/page.tsx +++ b/www/app/(app)/transcripts/[transcriptId]/upload/page.tsx @@ -24,10 +24,16 @@ const TranscriptUpload = (details: TranscriptUpload) => { const router = useRouter(); - const [status, setStatus] = useState( + const [status_, setStatus] = useState( webSockets.status.value || transcript.response?.status || "idle", ); + // status is obviously done if we have transcript + const status = + !transcript.loading && transcript.response?.status === "ended" + ? transcript.response?.status + : status_; + useEffect(() => { if (!transcriptStarted && webSockets.transcriptTextLive.length !== 0) setTranscriptStarted(true); @@ -35,8 +41,11 @@ const TranscriptUpload = (details: TranscriptUpload) => { useEffect(() => { //TODO HANDLE ERROR STATUS BETTER + // TODO deprecate webSockets.status.value / depend on transcript.response?.status from query lib const newStatus = - webSockets.status.value || transcript.response?.status || "idle"; + transcript.response?.status === "ended" + ? "ended" + : webSockets.status.value || transcript.response?.status || "idle"; setStatus(newStatus); if (newStatus && (newStatus == "ended" || newStatus == "error")) { console.log(newStatus, "redirecting"); diff --git a/www/app/(app)/transcripts/createTranscript.ts b/www/app/(app)/transcripts/createTranscript.ts index 015c82de..8a235161 100644 --- a/www/app/(app)/transcripts/createTranscript.ts +++ b/www/app/(app)/transcripts/createTranscript.ts @@ -1,45 +1,33 @@ -import { useEffect, useState } from "react"; +import type { components } from "../../reflector-api"; +import { useTranscriptCreate } from "../../lib/apiHooks"; -import { useError } from "../../(errors)/errorContext"; -import { CreateTranscript, GetTranscript } from "../../api"; -import useApi from "../../lib/useApi"; +type CreateTranscript = components["schemas"]["CreateTranscript"]; +type GetTranscript = components["schemas"]["GetTranscript"]; type UseCreateTranscript = { transcript: GetTranscript | null; loading: boolean; error: Error | null; - create: (transcriptCreationDetails: CreateTranscript) => void; + create: (transcriptCreationDetails: CreateTranscript) => Promise; }; const useCreateTranscript = (): UseCreateTranscript => { - const [transcript, setTranscript] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setErrorState] = useState(null); - const { setError } = useError(); - const api = useApi(); + const createMutation = useTranscriptCreate(); - const create = (transcriptCreationDetails: CreateTranscript) => { - if (loading || !api) return; + const create = async (transcriptCreationDetails: CreateTranscript) => { + if (createMutation.isPending) return; - setLoading(true); - - api - .v1TranscriptsCreate({ requestBody: transcriptCreationDetails }) - .then((transcript) => { - setTranscript(transcript); - setLoading(false); - }) - .catch((err) => { - setError( - err, - "There was an issue creating a transcript, please try again.", - ); - setErrorState(err); - setLoading(false); - }); + await createMutation.mutateAsync({ + body: transcriptCreationDetails, + }); }; - return { transcript, loading, error, create }; + return { + transcript: createMutation.data || null, + loading: createMutation.isPending, + error: createMutation.error as Error | null, + create, + }; }; export default useCreateTranscript; diff --git a/www/app/(app)/transcripts/fileUploadButton.tsx b/www/app/(app)/transcripts/fileUploadButton.tsx index 1b4101e8..1f5d72eb 100644 --- a/www/app/(app)/transcripts/fileUploadButton.tsx +++ b/www/app/(app)/transcripts/fileUploadButton.tsx @@ -1,6 +1,7 @@ import React, { useState } from "react"; -import useApi from "../../lib/useApi"; +import { useTranscriptUploadAudio } from "../../lib/apiHooks"; import { Button, Spinner } from "@chakra-ui/react"; +import { useError } from "../../(errors)/errorContext"; type FileUploadButton = { transcriptId: string; @@ -8,13 +9,16 @@ type FileUploadButton = { export default function FileUploadButton(props: FileUploadButton) { const fileInputRef = React.useRef(null); - const api = useApi(); + const uploadMutation = useTranscriptUploadAudio(); + const { setError } = useError(); const [progress, setProgress] = useState(0); const triggerFileUpload = () => { fileInputRef.current?.click(); }; - const handleFileUpload = (event: React.ChangeEvent) => { + const handleFileUpload = async ( + event: React.ChangeEvent, + ) => { const file = event.target.files?.[0]; if (file) { @@ -24,37 +28,45 @@ export default function FileUploadButton(props: FileUploadButton) { let start = 0; let uploadedSize = 0; - api?.httpRequest.config.interceptors.request.use((request) => { - request.onUploadProgress = (progressEvent) => { - const currentProgress = Math.floor( - ((uploadedSize + progressEvent.loaded) / file.size) * 100, - ); - setProgress(currentProgress); - }; - return request; - }); - const uploadNextChunk = async () => { - if (chunkNumber == totalChunks) return; + if (chunkNumber == totalChunks) { + setProgress(0); + return; + } const chunkSize = Math.min(maxChunkSize, file.size - start); const end = start + chunkSize; const chunk = file.slice(start, end); - await api?.v1TranscriptRecordUpload({ - transcriptId: props.transcriptId, - formData: { - chunk, - }, - chunkNumber, - totalChunks, - }); + try { + const formData = new FormData(); + formData.append("chunk", chunk); - uploadedSize += chunkSize; - chunkNumber++; - start = end; + await uploadMutation.mutateAsync({ + params: { + path: { + transcript_id: props.transcriptId, + }, + query: { + chunk_number: chunkNumber, + total_chunks: totalChunks, + }, + }, + body: formData as any, + }); - uploadNextChunk(); + uploadedSize += chunkSize; + const currentProgress = Math.floor((uploadedSize / file.size) * 100); + setProgress(currentProgress); + + chunkNumber++; + start = end; + + await uploadNextChunk(); + } catch (error) { + setError(error as Error, "Failed to upload file"); + setProgress(0); + } }; uploadNextChunk(); diff --git a/www/app/(app)/transcripts/new/page.tsx b/www/app/(app)/transcripts/new/page.tsx index 2670fd39..0410bd97 100644 --- a/www/app/(app)/transcripts/new/page.tsx +++ b/www/app/(app)/transcripts/new/page.tsx @@ -7,36 +7,29 @@ import About from "../../../(aboutAndPrivacy)/about"; import Privacy from "../../../(aboutAndPrivacy)/privacy"; import { useRouter } from "next/navigation"; import useCreateTranscript from "../createTranscript"; -import { SourceKind } from "../../../api"; import SelectSearch from "react-select-search"; import { supportedLanguages } from "../../../supportedLanguages"; -import useSessionStatus from "../../../lib/useSessionStatus"; import { featureEnabled } from "../../../domainContext"; -import { signIn } from "next-auth/react"; import { Flex, Box, Spinner, Heading, Button, - Card, Center, - Link, - CardBody, - Stack, Text, - Icon, - Grid, - IconButton, Spacer, - Menu, - Tooltip, - Input, } from "@chakra-ui/react"; +import { useAuth } from "../../../lib/AuthProvider"; +import type { components } from "../../../reflector-api"; + const TranscriptCreate = () => { const isClient = typeof window !== "undefined"; const router = useRouter(); - const { isLoading, isAuthenticated } = useSessionStatus(); + const auth = useAuth(); + const isAuthenticated = auth.status === "authenticated"; + const isAuthRefreshing = auth.status === "refreshing"; + const isLoading = auth.status === "loading"; const requireLogin = featureEnabled("requireLogin"); const [name, setName] = useState(""); @@ -55,27 +48,31 @@ const TranscriptCreate = () => { const [loadingUpload, setLoadingUpload] = useState(false); const getTargetLanguage = () => { - if (targetLanguage === "NOTRANSLATION") return; + if (targetLanguage === "NOTRANSLATION") return undefined; return targetLanguage; }; const send = () => { if (loadingRecord || createTranscript.loading || permissionDenied) return; setLoadingRecord(true); + const targetLang = getTargetLanguage(); createTranscript.create({ name, - target_language: getTargetLanguage(), - source_kind: "live" as SourceKind, + source_language: "en", + target_language: targetLang || "en", + source_kind: "live", }); }; const uploadFile = () => { if (loadingUpload || createTranscript.loading || permissionDenied) return; setLoadingUpload(true); + const targetLang = getTargetLanguage(); createTranscript.create({ name, - target_language: getTargetLanguage(), - source_kind: "file" as SourceKind, + source_language: "en", + target_language: targetLang || "en", + source_kind: "file", }); }; @@ -141,8 +138,8 @@ const TranscriptCreate = () => {
{isLoading ? ( - ) : requireLogin && !isAuthenticated ? ( - + ) : requireLogin && !isAuthenticated && !isAuthRefreshing ? ( + ) : ( { - if (!api) - throw new Error("ShareLink's API should always be ready at this point"); - const selectedOption = shareOptionsData.find( (option) => option.value === selectedValue, ); @@ -67,19 +66,27 @@ export default function ShareAndPrivacy(props: ShareAndPrivacyProps) { share_mode: selectedValue as "public" | "semi-private" | "private", }; - const updatedTranscript = await api.v1TranscriptUpdate({ - transcriptId: props.transcriptResponse.id, - requestBody, - }); - setShareMode( - shareOptionsData.find( - (option) => option.value === updatedTranscript.share_mode, - ) || shareOptionsData[0], - ); - setShareLoading(false); + try { + const updatedTranscript = await updateTranscriptMutation.mutateAsync({ + params: { + path: { transcript_id: props.transcriptResponse.id }, + }, + body: requestBody, + }); + setShareMode( + shareOptionsData.find( + (option) => option.value === updatedTranscript.share_mode, + ) || shareOptionsData[0], + ); + } catch (err) { + console.error("Failed to update share mode:", err); + } finally { + setShareLoading(false); + } }; - const userId = useSessionUser().id; + const auth = useAuth(); + const userId = auth.status === "authenticated" ? auth.user?.id : null; useEffect(() => { setIsOwner(!!(requireLogin && userId === props.transcriptResponse.user_id)); @@ -124,7 +131,7 @@ export default function ShareAndPrivacy(props: ShareAndPrivacyProps) { "This transcript is public. Everyone can access it."} - {isOwner && api && ( + {isOwner && ( (undefined); + const [selectedStreamId, setSelectedStreamId] = useState(null); const [topic, setTopic] = useState(undefined); const [includeTopics, setIncludeTopics] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const [streams, setStreams] = useState([]); - const [topics, setTopics] = useState([]); - const api = useApi(); + + const { data: streams = [], isLoading: isLoadingStreams } = useZulipStreams(); + const { data: topics = [] } = useZulipTopics(selectedStreamId); + const postToZulipMutation = useTranscriptPostToZulip(); + const { contains } = useFilter({ sensitivity: "base" }); - const { - collection: streamItemsCollection, - filter: streamItemsFilter, - set: streamItemsSet, - } = useListCollection({ - initialItems: [] as { label: string; value: string }[], - filter: contains, - }); + const streamItems = useMemo(() => { + return streams.map((stream: Stream) => ({ + label: stream.name, + value: stream.name, + })); + }, [streams]); - const { - collection: topicItemsCollection, - filter: topicItemsFilter, - set: topicItemsSet, - } = useListCollection({ - initialItems: [] as { label: string; value: string }[], - filter: contains, - }); + const topicItems = useMemo(() => { + return topics.map(({ name }) => ({ + label: name, + value: name, + })); + }, [topics]); + const { collection: streamItemsCollection, filter: streamItemsFilter } = + useListCollection({ + initialItems: streamItems, + filter: contains, + }); + + const { collection: topicItemsCollection, filter: topicItemsFilter } = + useListCollection({ + initialItems: topicItems, + filter: contains, + }); + + // Update selected stream ID when stream changes useEffect(() => { - const fetchZulipStreams = async () => { - if (!api) return; - - try { - const response = await api.v1ZulipGetStreams(); - setStreams(response); - - streamItemsSet( - response.map((stream) => ({ - label: stream.name, - value: stream.name, - })), - ); - - setIsLoading(false); - } catch (error) { - console.error("Error fetching Zulip streams:", error); - } - }; - - fetchZulipStreams(); - }, [!api]); - - useEffect(() => { - const fetchZulipTopics = async () => { - if (!api || !stream) return; - try { - const selectedStream = streams.find((s) => s.name === stream); - if (selectedStream) { - const response = await api.v1ZulipGetTopics({ - streamId: selectedStream.stream_id, - }); - setTopics(response); - topicItemsSet( - response.map((topic) => ({ - label: topic.name, - value: topic.name, - })), - ); - } else { - topicItemsSet([]); - } - } catch (error) { - console.error("Error fetching Zulip topics:", error); - } - }; - - fetchZulipTopics(); - }, [stream, streams, api]); + if (stream && streams) { + const selectedStream = streams.find((s: Stream) => s.name === stream); + setSelectedStreamId(selectedStream ? selectedStream.stream_id : null); + } else { + setSelectedStreamId(null); + } + }, [stream, streams]); const handleSendToZulip = async () => { - if (!api || !props.transcriptResponse) return; + if (!props.transcriptResponse) return; if (stream && topic) { try { - await api.v1TranscriptPostToZulip({ - transcriptId: props.transcriptResponse.id, - stream, - topic, - includeTopics, + await postToZulipMutation.mutateAsync({ + params: { + path: { + transcript_id: props.transcriptResponse.id, + }, + query: { + stream, + topic, + include_topics: includeTopics, + }, + }, }); setShowModal(false); } catch (error) { - console.log(error); + console.error("Error posting to Zulip:", error); } } }; @@ -155,7 +132,7 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) { - {isLoading ? ( + {isLoadingStreams ? ( diff --git a/www/app/(app)/transcripts/transcriptTitle.tsx b/www/app/(app)/transcripts/transcriptTitle.tsx index 4678818f..72421f48 100644 --- a/www/app/(app)/transcripts/transcriptTitle.tsx +++ b/www/app/(app)/transcripts/transcriptTitle.tsx @@ -1,6 +1,8 @@ import { useState } from "react"; -import { UpdateTranscript } from "../../api"; -import useApi from "../../lib/useApi"; +import type { components } from "../../reflector-api"; + +type UpdateTranscript = components["schemas"]["UpdateTranscript"]; +import { useTranscriptUpdate } from "../../lib/apiHooks"; import { Heading, IconButton, Input, Flex, Spacer } from "@chakra-ui/react"; import { LuPen } from "react-icons/lu"; @@ -14,24 +16,27 @@ const TranscriptTitle = (props: TranscriptTitle) => { const [displayedTitle, setDisplayedTitle] = useState(props.title); const [preEditTitle, setPreEditTitle] = useState(props.title); const [isEditing, setIsEditing] = useState(false); - const api = useApi(); + const updateTranscriptMutation = useTranscriptUpdate(); const updateTitle = async (newTitle: string, transcriptId: string) => { - if (!api) return; try { const requestBody: UpdateTranscript = { title: newTitle, }; - const updatedTranscript = await api?.v1TranscriptUpdate({ - transcriptId, - requestBody, + await updateTranscriptMutation.mutateAsync({ + params: { + path: { transcript_id: transcriptId }, + }, + body: requestBody, }); if (props.onUpdate) { props.onUpdate(newTitle); } - console.log("Updated transcript:", updatedTranscript); + console.log("Updated transcript title:", newTitle); } catch (err) { console.error("Failed to update transcript:", err); + // Revert title on error + setDisplayedTitle(preEditTitle); } }; diff --git a/www/app/(app)/transcripts/useMp3.ts b/www/app/(app)/transcripts/useMp3.ts index 3e8344ad..223a9a4a 100644 --- a/www/app/(app)/transcripts/useMp3.ts +++ b/www/app/(app)/transcripts/useMp3.ts @@ -1,6 +1,7 @@ import { useContext, useEffect, useState } from "react"; import { DomainContext } from "../../domainContext"; -import getApi from "../../lib/useApi"; +import { useTranscriptGet } from "../../lib/apiHooks"; +import { useAuth } from "../../lib/AuthProvider"; export type Mp3Response = { media: HTMLMediaElement | null; @@ -17,14 +18,17 @@ const useMp3 = (transcriptId: string, waiting?: boolean): Mp3Response => { const [audioLoadingError, setAudioLoadingError] = useState( null, ); - const [transcriptMetadataLoading, setTranscriptMetadataLoading] = - useState(true); - const [transcriptMetadataLoadingError, setTranscriptMetadataLoadingError] = - useState(null); const [audioDeleted, setAudioDeleted] = useState(null); - const api = getApi(); const { api_url } = useContext(DomainContext); - const accessTokenInfo = api?.httpRequest?.config?.TOKEN; + const auth = useAuth(); + const accessTokenInfo = + auth.status === "authenticated" ? auth.accessToken : null; + + const { + data: transcript, + isLoading: transcriptMetadataLoading, + error: transcriptError, + } = useTranscriptGet(later ? null : transcriptId); const [serviceWorker, setServiceWorker] = useState(null); @@ -52,72 +56,50 @@ const useMp3 = (transcriptId: string, waiting?: boolean): Mp3Response => { }, [navigator.serviceWorker, !serviceWorker, accessTokenInfo]); useEffect(() => { - if (!transcriptId || !api || later) return; + if (!transcriptId || later || !transcript) return; let stopped = false; let audioElement: HTMLAudioElement | null = null; let handleCanPlay: (() => void) | null = null; let handleError: (() => void) | null = null; - setTranscriptMetadataLoading(true); setAudioLoading(true); - // First fetch transcript info to check if audio is deleted - api - .v1TranscriptGet({ transcriptId }) - .then((transcript) => { - if (stopped) { - return; - } + const deleted = transcript.audio_deleted || false; + setAudioDeleted(deleted); - const deleted = transcript.audio_deleted || false; - setAudioDeleted(deleted); - setTranscriptMetadataLoadingError(null); + if (deleted) { + // Audio is deleted, don't attempt to load it + setMedia(null); + setAudioLoadingError(null); + setAudioLoading(false); + return; + } - if (deleted) { - // Audio is deleted, don't attempt to load it - setMedia(null); - setAudioLoadingError(null); - setAudioLoading(false); - return; - } + // Audio is not deleted, proceed to load it + audioElement = document.createElement("audio"); + audioElement.src = `${api_url}/v1/transcripts/${transcriptId}/audio/mp3`; + audioElement.crossOrigin = "anonymous"; + audioElement.preload = "auto"; - // Audio is not deleted, proceed to load it - audioElement = document.createElement("audio"); - audioElement.src = `${api_url}/v1/transcripts/${transcriptId}/audio/mp3`; - audioElement.crossOrigin = "anonymous"; - audioElement.preload = "auto"; + handleCanPlay = () => { + if (stopped) return; + setAudioLoading(false); + setAudioLoadingError(null); + }; - handleCanPlay = () => { - if (stopped) return; - setAudioLoading(false); - setAudioLoadingError(null); - }; + handleError = () => { + if (stopped) return; + setAudioLoading(false); + setAudioLoadingError("Failed to load audio"); + }; - handleError = () => { - if (stopped) return; - setAudioLoading(false); - setAudioLoadingError("Failed to load audio"); - }; + audioElement.addEventListener("canplay", handleCanPlay); + audioElement.addEventListener("error", handleError); - audioElement.addEventListener("canplay", handleCanPlay); - audioElement.addEventListener("error", handleError); - - if (!stopped) { - setMedia(audioElement); - } - }) - .catch((error) => { - if (stopped) return; - console.error("Failed to fetch transcript:", error); - setAudioDeleted(null); - setTranscriptMetadataLoadingError(error.message); - setAudioLoading(false); - }) - .finally(() => { - if (stopped) return; - setTranscriptMetadataLoading(false); - }); + if (!stopped) { + setMedia(audioElement); + } return () => { stopped = true; @@ -128,14 +110,18 @@ const useMp3 = (transcriptId: string, waiting?: boolean): Mp3Response => { if (handleError) audioElement.removeEventListener("error", handleError); } }; - }, [transcriptId, api, later, api_url]); + }, [transcriptId, transcript, later, api_url]); const getNow = () => { setLater(false); }; const loading = audioLoading || transcriptMetadataLoading; - const error = audioLoadingError || transcriptMetadataLoadingError; + const error = + audioLoadingError || + (transcriptError + ? (transcriptError as any).message || String(transcriptError) + : null); return { media, loading, error, getNow, audioDeleted }; }; diff --git a/www/app/(app)/transcripts/useParticipants.ts b/www/app/(app)/transcripts/useParticipants.ts index 38f5aa35..a3674597 100644 --- a/www/app/(app)/transcripts/useParticipants.ts +++ b/www/app/(app)/transcripts/useParticipants.ts @@ -1,8 +1,6 @@ -import { useEffect, useState } from "react"; -import { Participant } from "../../api"; -import { useError } from "../../(errors)/errorContext"; -import useApi from "../../lib/useApi"; -import { shouldShowError } from "../../lib/errorUtils"; +import type { components } from "../../reflector-api"; +type Participant = components["schemas"]["Participant"]; +import { useTranscriptParticipants } from "../../lib/apiHooks"; type ErrorParticipants = { error: Error; @@ -29,46 +27,38 @@ export type UseParticipants = ( ) & { refetch: () => void }; const useParticipants = (transcriptId: string): UseParticipants => { - const [response, setResponse] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setErrorState] = useState(null); - const { setError } = useError(); - const api = useApi(); - const [count, setCount] = useState(0); + const { + data: response, + isLoading: loading, + error, + refetch, + } = useTranscriptParticipants(transcriptId || null); - const refetch = () => { - if (!loading) { - setCount(count + 1); - setLoading(true); - setErrorState(null); - } - }; + // Type-safe return based on state + if (error) { + return { + error: error as Error, + loading: false, + response: null, + refetch, + } satisfies ErrorParticipants & { refetch: () => void }; + } - useEffect(() => { - if (!transcriptId || !api) return; + if (loading || !response) { + return { + response: response || null, + loading: true, + error: null, + refetch, + } satisfies LoadingParticipants & { refetch: () => void }; + } - setLoading(true); - api - .v1TranscriptGetParticipants({ transcriptId }) - .then((result) => { - setResponse(result); - setLoading(false); - console.debug("Participants Loaded:", result); - }) - .catch((error) => { - const shouldShowHuman = shouldShowError(error); - if (shouldShowHuman) { - setError(error, "There was an error loading the participants"); - } else { - setError(error); - } - setErrorState(error); - setResponse(null); - setLoading(false); - }); - }, [transcriptId, !api, count]); - - return { response, loading, error, refetch } as UseParticipants; + return { + response, + loading: false, + error: null, + refetch, + } satisfies SuccessParticipants & { refetch: () => void }; }; export default useParticipants; diff --git a/www/app/(app)/transcripts/useSearchTranscripts.ts b/www/app/(app)/transcripts/useSearchTranscripts.ts deleted file mode 100644 index 2e6a7311..00000000 --- a/www/app/(app)/transcripts/useSearchTranscripts.ts +++ /dev/null @@ -1,123 +0,0 @@ -// this hook is not great, we want to substitute it with a proper state management solution that is also not re-invention - -import { useEffect, useRef, useState } from "react"; -import { SearchResult, SourceKind } from "../../api"; -import useApi from "../../lib/useApi"; -import { - PaginationPage, - paginationPageTo0Based, -} from "../browse/_components/Pagination"; - -interface SearchFilters { - roomIds: readonly string[] | null; - sourceKind: SourceKind | null; -} - -const EMPTY_SEARCH_FILTERS: SearchFilters = { - roomIds: null, - sourceKind: null, -}; - -type UseSearchTranscriptsOptions = { - pageSize: number; - page: PaginationPage; -}; - -interface UseSearchTranscriptsReturn { - results: SearchResult[]; - totalCount: number; - isLoading: boolean; - error: unknown; - reload: () => void; -} - -function hashEffectFilters(filters: SearchFilters): string { - return JSON.stringify(filters); -} - -export function useSearchTranscripts( - query: string = "", - filters: SearchFilters = EMPTY_SEARCH_FILTERS, - options: UseSearchTranscriptsOptions = { - pageSize: 20, - page: PaginationPage(1), - }, -): UseSearchTranscriptsReturn { - const { pageSize, page } = options; - - const [reloadCount, setReloadCount] = useState(0); - - const api = useApi(); - const abortControllerRef = useRef(); - - const [data, setData] = useState<{ results: SearchResult[]; total: number }>({ - results: [], - total: 0, - }); - const [error, setError] = useState(); - const [isLoading, setIsLoading] = useState(false); - - const filterHash = hashEffectFilters(filters); - - useEffect(() => { - if (!api) { - setData({ results: [], total: 0 }); - setError(undefined); - setIsLoading(false); - return; - } - - if (abortControllerRef.current) { - abortControllerRef.current.abort(); - } - - const abortController = new AbortController(); - abortControllerRef.current = abortController; - - const performSearch = async () => { - setIsLoading(true); - - try { - const response = await api.v1TranscriptsSearch({ - q: query || "", - limit: pageSize, - offset: paginationPageTo0Based(page) * pageSize, - roomId: filters.roomIds?.[0], - sourceKind: filters.sourceKind || undefined, - }); - - if (abortController.signal.aborted) return; - setData(response); - setError(undefined); - } catch (err: unknown) { - if ((err as Error).name === "AbortError") { - return; - } - if (abortController.signal.aborted) { - console.error("Aborted search but error", err); - return; - } - - setError(err); - } finally { - if (!abortController.signal.aborted) { - setIsLoading(false); - } - } - }; - - performSearch().then(() => {}); - - return () => { - abortController.abort(); - }; - }, [api, query, page, filterHash, pageSize, reloadCount]); - - return { - results: data.results, - totalCount: data.total, - isLoading, - error, - reload: () => setReloadCount(reloadCount + 1), - }; -} diff --git a/www/app/(app)/transcripts/useTopicWithWords.ts b/www/app/(app)/transcripts/useTopicWithWords.ts index 29d0b982..31e184cc 100644 --- a/www/app/(app)/transcripts/useTopicWithWords.ts +++ b/www/app/(app)/transcripts/useTopicWithWords.ts @@ -1,9 +1,8 @@ -import { useEffect, useState } from "react"; +import type { components } from "../../reflector-api"; +import { useTranscriptTopicsWithWordsPerSpeaker } from "../../lib/apiHooks"; -import { GetTranscriptTopicWithWordsPerSpeaker } from "../../api"; -import { useError } from "../../(errors)/errorContext"; -import useApi from "../../lib/useApi"; -import { shouldShowError } from "../../lib/errorUtils"; +type GetTranscriptTopicWithWordsPerSpeaker = + components["schemas"]["GetTranscriptTopicWithWordsPerSpeaker"]; type ErrorTopicWithWords = { error: Error; @@ -33,47 +32,40 @@ const useTopicWithWords = ( topicId: string | undefined, transcriptId: string, ): UseTopicWithWords => { - const [response, setResponse] = - useState(null); - const [loading, setLoading] = useState(false); - const [error, setErrorState] = useState(null); - const { setError } = useError(); - const api = useApi(); + const { + data: response, + isLoading: loading, + error, + refetch, + } = useTranscriptTopicsWithWordsPerSpeaker( + transcriptId || null, + topicId || null, + ); - const [count, setCount] = useState(0); + if (error) { + return { + error: error as Error, + loading: false, + response: null, + refetch, + } satisfies ErrorTopicWithWords & { refetch: () => void }; + } - const refetch = () => { - if (!loading) { - setCount(count + 1); - setLoading(true); - setErrorState(null); - } - }; + if (loading || !response) { + return { + response: response || null, + loading: true, + error: false, + refetch, + } satisfies LoadingTopicWithWords & { refetch: () => void }; + } - useEffect(() => { - if (!transcriptId || !topicId || !api) return; - - setLoading(true); - - api - .v1TranscriptGetTopicsWithWordsPerSpeaker({ transcriptId, topicId }) - .then((result) => { - setResponse(result); - setLoading(false); - console.debug("Topics with words Loaded:", result); - }) - .catch((error) => { - const shouldShowHuman = shouldShowError(error); - if (shouldShowHuman) { - setError(error, "There was an error loading the topics with words"); - } else { - setError(error); - } - setErrorState(error); - }); - }, [transcriptId, !api, topicId, count]); - - return { response, loading, error, refetch } as UseTopicWithWords; + return { + response, + loading: false, + error: null, + refetch, + } satisfies SuccessTopicWithWords & { refetch: () => void }; }; export default useTopicWithWords; diff --git a/www/app/(app)/transcripts/useTopics.ts b/www/app/(app)/transcripts/useTopics.ts index ff17beaf..7f337582 100644 --- a/www/app/(app)/transcripts/useTopics.ts +++ b/www/app/(app)/transcripts/useTopics.ts @@ -1,10 +1,7 @@ -import { useEffect, useState } from "react"; +import { useTranscriptTopics } from "../../lib/apiHooks"; +import type { components } from "../../reflector-api"; -import { useError } from "../../(errors)/errorContext"; -import { Topic } from "./webSocketTypes"; -import useApi from "../../lib/useApi"; -import { shouldShowError } from "../../lib/errorUtils"; -import { GetTranscriptTopic } from "../../api"; +type GetTranscriptTopic = components["schemas"]["GetTranscriptTopic"]; type TranscriptTopics = { topics: GetTranscriptTopic[] | null; @@ -13,34 +10,13 @@ type TranscriptTopics = { }; const useTopics = (id: string): TranscriptTopics => { - const [topics, setTopics] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setErrorState] = useState(null); - const { setError } = useError(); - const api = useApi(); - useEffect(() => { - if (!id || !api) return; + const { data: topics, isLoading: loading, error } = useTranscriptTopics(id); - setLoading(true); - api - .v1TranscriptGetTopics({ transcriptId: id }) - .then((result) => { - setTopics(result); - setLoading(false); - console.debug("Transcript topics loaded:", result); - }) - .catch((err) => { - setErrorState(err); - const shouldShowHuman = shouldShowError(err); - if (shouldShowHuman) { - setError(err, "There was an error loading the topics"); - } else { - setError(err); - } - }); - }, [id, !api]); - - return { topics, loading, error }; + return { + topics: topics || null, + loading, + error: error as Error | null, + }; }; export default useTopics; diff --git a/www/app/(app)/transcripts/useTranscript.ts b/www/app/(app)/transcripts/useTranscript.ts index 49d257f0..3e56fb9e 100644 --- a/www/app/(app)/transcripts/useTranscript.ts +++ b/www/app/(app)/transcripts/useTranscript.ts @@ -1,8 +1,7 @@ -import { useEffect, useState } from "react"; -import { GetTranscript } from "../../api"; -import { useError } from "../../(errors)/errorContext"; -import { shouldShowError } from "../../lib/errorUtils"; -import useApi from "../../lib/useApi"; +import type { components } from "../../reflector-api"; +import { useTranscriptGet } from "../../lib/apiHooks"; + +type GetTranscript = components["schemas"]["GetTranscript"]; type ErrorTranscript = { error: Error; @@ -28,43 +27,43 @@ type SuccessTranscript = { const useTranscript = ( id: string | null, ): ErrorTranscript | LoadingTranscript | SuccessTranscript => { - const [response, setResponse] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setErrorState] = useState(null); - const [reload, setReload] = useState(0); - const { setError } = useError(); - const api = useApi(); - const reloadHandler = () => setReload((prev) => prev + 1); + const { data, isLoading, error, refetch } = useTranscriptGet(id); - useEffect(() => { - if (!id || !api) return; + // Map to the expected return format + if (isLoading) { + return { + response: null, + loading: true, + error: false, + reload: refetch, + }; + } - if (!response) { - setLoading(true); - } + if (error) { + return { + error: error as Error, + loading: false, + response: null, + reload: refetch, + }; + } - api - .v1TranscriptGet({ transcriptId: id }) - .then((result) => { - setResponse(result); - setLoading(false); - console.debug("Transcript Loaded:", result); - }) - .catch((error) => { - const shouldShowHuman = shouldShowError(error); - if (shouldShowHuman) { - setError(error, "There was an error loading the transcript"); - } else { - setError(error); - } - setErrorState(error); - }); - }, [id, !api, reload]); + // Check if data is undefined or null + if (!data) { + return { + response: null, + loading: true, + error: false, + reload: refetch, + }; + } - return { response, loading, error, reload: reloadHandler } as - | ErrorTranscript - | LoadingTranscript - | SuccessTranscript; + return { + response: data, + loading: false, + error: null, + reload: refetch, + }; }; export default useTranscript; diff --git a/www/app/(app)/transcripts/useWaveform.ts b/www/app/(app)/transcripts/useWaveform.ts index 19b2a265..8bb8c4c9 100644 --- a/www/app/(app)/transcripts/useWaveform.ts +++ b/www/app/(app)/transcripts/useWaveform.ts @@ -1,8 +1,7 @@ -import { useEffect, useState } from "react"; -import { AudioWaveform } from "../../api"; -import { useError } from "../../(errors)/errorContext"; -import useApi from "../../lib/useApi"; -import { shouldShowError } from "../../lib/errorUtils"; +import type { components } from "../../reflector-api"; +import { useTranscriptWaveform } from "../../lib/apiHooks"; + +type AudioWaveform = components["schemas"]["AudioWaveform"]; type AudioWaveFormResponse = { waveform: AudioWaveform | null; @@ -11,35 +10,17 @@ type AudioWaveFormResponse = { }; const useWaveform = (id: string, skip: boolean): AudioWaveFormResponse => { - const [waveform, setWaveform] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setErrorState] = useState(null); - const { setError } = useError(); - const api = useApi(); + const { + data: waveform, + isLoading: loading, + error, + } = useTranscriptWaveform(skip ? null : id); - useEffect(() => { - if (!id || !api || skip) { - setLoading(false); - setErrorState(null); - setWaveform(null); - return; - } - setLoading(true); - setErrorState(null); - api - .v1TranscriptGetAudioWaveform({ transcriptId: id }) - .then((result) => { - setWaveform(result); - setLoading(false); - console.debug("Transcript waveform loaded:", result); - }) - .catch((err) => { - setErrorState(err); - setLoading(false); - }); - }, [id, api, skip]); - - return { waveform, loading, error }; + return { + waveform: waveform || null, + loading, + error: error as Error | null, + }; }; export default useWaveform; diff --git a/www/app/(app)/transcripts/useWebRTC.ts b/www/app/(app)/transcripts/useWebRTC.ts index c8370aa4..89a2a946 100644 --- a/www/app/(app)/transcripts/useWebRTC.ts +++ b/www/app/(app)/transcripts/useWebRTC.ts @@ -1,8 +1,9 @@ import { useEffect, useState } from "react"; import Peer from "simple-peer"; import { useError } from "../../(errors)/errorContext"; -import useApi from "../../lib/useApi"; -import { RtcOffer } from "../../api"; +import { useTranscriptWebRTC } from "../../lib/apiHooks"; +import type { components } from "../../reflector-api"; +type RtcOffer = components["schemas"]["RtcOffer"]; const useWebRTC = ( stream: MediaStream | null, @@ -10,10 +11,10 @@ const useWebRTC = ( ): Peer => { const [peer, setPeer] = useState(null); const { setError } = useError(); - const api = useApi(); + const { mutateAsync: mutateWebRtcTranscriptAsync } = useTranscriptWebRTC(); useEffect(() => { - if (!stream || !transcriptId || !api) { + if (!stream || !transcriptId) { return; } @@ -24,7 +25,7 @@ const useWebRTC = ( try { p = new Peer({ initiator: true, stream: stream }); } catch (error) { - setError(error, "Error creating WebRTC"); + setError(error as Error, "Error creating WebRTC"); return; } @@ -32,26 +33,31 @@ const useWebRTC = ( setError(new Error(`WebRTC error: ${err}`)); }); - p.on("signal", (data: any) => { - if (!api) return; + p.on("signal", async (data: any) => { if ("sdp" in data) { const rtcOffer: RtcOffer = { sdp: data.sdp, type: data.type, }; - api - .v1TranscriptRecordWebrtc({ transcriptId, requestBody: rtcOffer }) - .then((answer) => { - try { - p.signal(answer); - } catch (error) { - setError(error); - } - }) - .catch((error) => { - setError(error, "Error loading WebRTCOffer"); + try { + const answer = await mutateWebRtcTranscriptAsync({ + params: { + path: { + transcript_id: transcriptId, + }, + }, + body: rtcOffer, }); + + try { + p.signal(answer); + } catch (error) { + setError(error as Error); + } + } catch (error) { + setError(error as Error, "Error loading WebRTCOffer"); + } } }); @@ -63,7 +69,7 @@ const useWebRTC = ( return () => { p.destroy(); }; - }, [stream, transcriptId, !api]); + }, [stream, transcriptId, mutateWebRtcTranscriptAsync]); return peer; }; diff --git a/www/app/(app)/transcripts/useWebSockets.ts b/www/app/(app)/transcripts/useWebSockets.ts index 6fa5edc7..2b3205c4 100644 --- a/www/app/(app)/transcripts/useWebSockets.ts +++ b/www/app/(app)/transcripts/useWebSockets.ts @@ -2,8 +2,12 @@ import { useContext, useEffect, useState } from "react"; import { Topic, FinalSummary, Status } from "./webSocketTypes"; import { useError } from "../../(errors)/errorContext"; import { DomainContext } from "../../domainContext"; -import { AudioWaveform, GetTranscriptSegmentTopic } from "../../api"; -import useApi from "../../lib/useApi"; +import type { components } from "../../reflector-api"; +type AudioWaveform = components["schemas"]["AudioWaveform"]; +type GetTranscriptSegmentTopic = + components["schemas"]["GetTranscriptSegmentTopic"]; +import { useQueryClient } from "@tanstack/react-query"; +import { $api } from "../../lib/apiClient"; export type UseWebSockets = { transcriptTextLive: string; @@ -33,8 +37,8 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { const [status, setStatus] = useState({ value: "" }); const { setError } = useError(); - const { websocket_url } = useContext(DomainContext); - const api = useApi(); + const { websocket_url: websocketUrl } = useContext(DomainContext); + const queryClient = useQueryClient(); const [accumulatedText, setAccumulatedText] = useState(""); @@ -105,6 +109,13 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { title: "Topic 1: Introduction to Quantum Mechanics", transcript: "A brief overview of quantum mechanics and its principles.", + segments: [ + { + speaker: 1, + start: 0, + text: "This is the transcription of an example title", + }, + ], }, { id: "2", @@ -315,11 +326,9 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { } }; - if (!transcriptId || !api) return; + if (!transcriptId) return; - api?.v1TranscriptGetWebsocketEvents({ transcriptId }).then((result) => {}); - - const url = `${websocket_url}/v1/transcripts/${transcriptId}/events`; + const url = `${websocketUrl}/v1/transcripts/${transcriptId}/events`; let ws = new WebSocket(url); ws.onopen = () => { @@ -361,6 +370,16 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { return [...prevTopics, topic]; }); console.debug("TOPIC event:", message.data); + // Invalidate topics query to sync with WebSocket data + queryClient.invalidateQueries({ + queryKey: $api.queryOptions( + "get", + "/v1/transcripts/{transcript_id}/topics", + { + params: { path: { transcript_id: transcriptId } }, + }, + ).queryKey, + }); break; case "FINAL_SHORT_SUMMARY": @@ -370,6 +389,16 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { case "FINAL_LONG_SUMMARY": if (message.data) { setFinalSummary(message.data); + // Invalidate transcript query to sync summary + queryClient.invalidateQueries({ + queryKey: $api.queryOptions( + "get", + "/v1/transcripts/{transcript_id}", + { + params: { path: { transcript_id: transcriptId } }, + }, + ).queryKey, + }); } break; @@ -377,6 +406,16 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { console.debug("FINAL_TITLE event:", message.data); if (message.data) { setTitle(message.data.title); + // Invalidate transcript query to sync title + queryClient.invalidateQueries({ + queryKey: $api.queryOptions( + "get", + "/v1/transcripts/{transcript_id}", + { + params: { path: { transcript_id: transcriptId } }, + }, + ).queryKey, + }); } break; @@ -434,6 +473,11 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { break; case 1001: // Navigate away break; + case 1006: // Closed by client Chrome + console.warn( + "WebSocket closed by client, likely duplicated connection in react dev mode", + ); + break; default: setError( new Error(`WebSocket closed unexpectedly with code: ${event.code}`), @@ -450,7 +494,7 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => { return () => { ws.close(); }; - }, [transcriptId, !api]); + }, [transcriptId, websocketUrl]); return { transcriptTextLive, diff --git a/www/app/(app)/transcripts/webSocketTypes.ts b/www/app/(app)/transcripts/webSocketTypes.ts index edd35eb6..4ec98946 100644 --- a/www/app/(app)/transcripts/webSocketTypes.ts +++ b/www/app/(app)/transcripts/webSocketTypes.ts @@ -1,4 +1,6 @@ -import { GetTranscriptTopic } from "../../api"; +import type { components } from "../../reflector-api"; + +type GetTranscriptTopic = components["schemas"]["GetTranscriptTopic"]; export type Topic = GetTranscriptTopic; diff --git a/www/app/(auth)/userInfo.tsx b/www/app/(auth)/userInfo.tsx index ffb286b3..bf6a5b62 100644 --- a/www/app/(auth)/userInfo.tsx +++ b/www/app/(auth)/userInfo.tsx @@ -1,18 +1,21 @@ "use client"; -import { signOut, signIn } from "next-auth/react"; -import useSessionStatus from "../lib/useSessionStatus"; + import { Spinner, Link } from "@chakra-ui/react"; +import { useAuth } from "../lib/AuthProvider"; export default function UserInfo() { - const { isLoading, isAuthenticated } = useSessionStatus(); - + const auth = useAuth(); + const status = auth.status; + const isLoading = status === "loading"; + const isAuthenticated = status === "authenticated"; + const isRefreshing = status === "refreshing"; return isLoading ? ( - ) : !isAuthenticated ? ( + ) : !isAuthenticated && !isRefreshing ? ( signIn("authentik")} + onClick={() => auth.signIn("authentik")} > Log in @@ -20,7 +23,7 @@ export default function UserInfo() { signOut({ callbackUrl: "/" })} + onClick={() => auth.signOut({ callbackUrl: "/" })} > Log out diff --git a/www/app/[roomName]/page.tsx b/www/app/[roomName]/page.tsx index b03a7e4f..0130588b 100644 --- a/www/app/[roomName]/page.tsx +++ b/www/app/[roomName]/page.tsx @@ -21,11 +21,13 @@ import { toaster } from "../components/ui/toaster"; import useRoomMeeting from "./useRoomMeeting"; import { useRouter } from "next/navigation"; import { notFound } from "next/navigation"; -import useSessionStatus from "../lib/useSessionStatus"; import { useRecordingConsent } from "../recordingConsentContext"; -import useApi from "../lib/useApi"; -import { Meeting } from "../api"; +import { useMeetingAudioConsent } from "../lib/apiHooks"; +import type { components } from "../reflector-api"; + +type Meeting = components["schemas"]["Meeting"]; import { FaBars } from "react-icons/fa6"; +import { useAuth } from "../lib/AuthProvider"; export type RoomDetails = { params: { @@ -76,31 +78,30 @@ const useConsentDialog = ( wherebyRef: RefObject /*accessibility*/, ) => { const { state: consentState, touch, hasConsent } = useRecordingConsent(); - const [consentLoading, setConsentLoading] = useState(false); // toast would open duplicates, even with using "id=" prop const [modalOpen, setModalOpen] = useState(false); - const api = useApi(); + const audioConsentMutation = useMeetingAudioConsent(); const handleConsent = useCallback( async (meetingId: string, given: boolean) => { - if (!api) return; - - setConsentLoading(true); - try { - await api.v1MeetingAudioConsent({ - meetingId, - requestBody: { consent_given: given }, + await audioConsentMutation.mutateAsync({ + params: { + path: { + meeting_id: meetingId, + }, + }, + body: { + consent_given: given, + }, }); touch(meetingId); } catch (error) { console.error("Error submitting consent:", error); - } finally { - setConsentLoading(false); } }, - [api, touch], + [audioConsentMutation, touch], ); const showConsentModal = useCallback(() => { @@ -194,7 +195,12 @@ const useConsentDialog = ( return cleanup; }, [meetingId, handleConsent, wherebyRef, modalOpen]); - return { showConsentModal, consentState, hasConsent, consentLoading }; + return { + showConsentModal, + consentState, + hasConsent, + consentLoading: audioConsentMutation.isPending, + }; }; function ConsentDialogButton({ @@ -254,7 +260,9 @@ export default function Room(details: RoomDetails) { const roomName = details.params.roomName; const meeting = useRoomMeeting(roomName); const router = useRouter(); - const { isLoading, isAuthenticated } = useSessionStatus(); + const status = useAuth().status; + const isAuthenticated = status === "authenticated"; + const isLoading = status === "loading" || meeting.loading; const roomUrl = meeting?.response?.host_room_url ? meeting?.response?.host_room_url diff --git a/www/app/[roomName]/useRoomMeeting.tsx b/www/app/[roomName]/useRoomMeeting.tsx index 98c2f1f2..93491a05 100644 --- a/www/app/[roomName]/useRoomMeeting.tsx +++ b/www/app/[roomName]/useRoomMeeting.tsx @@ -1,8 +1,10 @@ import { useEffect, useState } from "react"; import { useError } from "../(errors)/errorContext"; -import { Meeting } from "../api"; +import type { components } from "../reflector-api"; import { shouldShowError } from "../lib/errorUtils"; -import useApi from "../lib/useApi"; + +type Meeting = components["schemas"]["Meeting"]; +import { useRoomsCreateMeeting } from "../lib/apiHooks"; import { notFound } from "next/navigation"; type ErrorMeeting = { @@ -30,27 +32,25 @@ const useRoomMeeting = ( roomName: string | null | undefined, ): ErrorMeeting | LoadingMeeting | SuccessMeeting => { const [response, setResponse] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setErrorState] = useState(null); const [reload, setReload] = useState(0); const { setError } = useError(); - const api = useApi(); + const createMeetingMutation = useRoomsCreateMeeting(); const reloadHandler = () => setReload((prev) => prev + 1); useEffect(() => { - if (!roomName || !api) return; + if (!roomName) return; - if (!response) { - setLoading(true); - } - - api - .v1RoomsCreateMeeting({ roomName }) - .then((result) => { + const createMeeting = async () => { + try { + const result = await createMeetingMutation.mutateAsync({ + params: { + path: { + room_name: roomName, + }, + }, + }); setResponse(result); - setLoading(false); - }) - .catch((error) => { + } catch (error: any) { const shouldShowHuman = shouldShowError(error); if (shouldShowHuman && error.status !== 404) { setError( @@ -60,9 +60,14 @@ const useRoomMeeting = ( } else { setError(error); } - setErrorState(error); - }); - }, [roomName, !api, reload]); + } + }; + + createMeeting(); + }, [roomName, reload]); + + const loading = createMeetingMutation.isPending && !response; + const error = createMeetingMutation.error as Error | null; return { response, loading, error, reload: reloadHandler } as | ErrorMeeting diff --git a/www/app/api/OpenApi.ts b/www/app/api/OpenApi.ts deleted file mode 100644 index 23cc35f3..00000000 --- a/www/app/api/OpenApi.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { BaseHttpRequest } from "./core/BaseHttpRequest"; -import type { OpenAPIConfig } from "./core/OpenAPI"; -import { Interceptors } from "./core/OpenAPI"; -import { AxiosHttpRequest } from "./core/AxiosHttpRequest"; - -import { DefaultService } from "./services.gen"; - -type HttpRequestConstructor = new (config: OpenAPIConfig) => BaseHttpRequest; - -export class OpenApi { - public readonly default: DefaultService; - - public readonly request: BaseHttpRequest; - - constructor( - config?: Partial, - HttpRequest: HttpRequestConstructor = AxiosHttpRequest, - ) { - this.request = new HttpRequest({ - BASE: config?.BASE ?? "", - VERSION: config?.VERSION ?? "0.1.0", - WITH_CREDENTIALS: config?.WITH_CREDENTIALS ?? false, - CREDENTIALS: config?.CREDENTIALS ?? "include", - TOKEN: config?.TOKEN, - USERNAME: config?.USERNAME, - PASSWORD: config?.PASSWORD, - HEADERS: config?.HEADERS, - ENCODE_PATH: config?.ENCODE_PATH, - interceptors: { - request: config?.interceptors?.request ?? new Interceptors(), - response: config?.interceptors?.response ?? new Interceptors(), - }, - }); - - this.default = new DefaultService(this.request); - } -} diff --git a/www/app/api/auth/[...nextauth]/route.ts b/www/app/api/auth/[...nextauth]/route.ts index 915ed04d..7b73c22a 100644 --- a/www/app/api/auth/[...nextauth]/route.ts +++ b/www/app/api/auth/[...nextauth]/route.ts @@ -1,8 +1,5 @@ -// NextAuth route handler for Authentik -// Refresh rotation has been taken from https://next-auth.js.org/v3/tutorials/refresh-token-rotation even if we are using 4.x - import NextAuth from "next-auth"; -import { authOptions } from "../../../lib/auth"; +import { authOptions } from "../../../lib/authBackend"; const handler = NextAuth(authOptions); diff --git a/www/app/api/core/ApiError.ts b/www/app/api/core/ApiError.ts deleted file mode 100644 index 1d07bb31..00000000 --- a/www/app/api/core/ApiError.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { ApiRequestOptions } from "./ApiRequestOptions"; -import type { ApiResult } from "./ApiResult"; - -export class ApiError extends Error { - public readonly url: string; - public readonly status: number; - public readonly statusText: string; - public readonly body: unknown; - public readonly request: ApiRequestOptions; - - constructor( - request: ApiRequestOptions, - response: ApiResult, - message: string, - ) { - super(message); - - this.name = "ApiError"; - this.url = response.url; - this.status = response.status; - this.statusText = response.statusText; - this.body = response.body; - this.request = request; - } -} diff --git a/www/app/api/core/ApiRequestOptions.ts b/www/app/api/core/ApiRequestOptions.ts deleted file mode 100644 index 57fbb095..00000000 --- a/www/app/api/core/ApiRequestOptions.ts +++ /dev/null @@ -1,21 +0,0 @@ -export type ApiRequestOptions = { - readonly method: - | "GET" - | "PUT" - | "POST" - | "DELETE" - | "OPTIONS" - | "HEAD" - | "PATCH"; - readonly url: string; - readonly path?: Record; - readonly cookies?: Record; - readonly headers?: Record; - readonly query?: Record; - readonly formData?: Record; - readonly body?: any; - readonly mediaType?: string; - readonly responseHeader?: string; - readonly responseTransformer?: (data: unknown) => Promise; - readonly errors?: Record; -}; diff --git a/www/app/api/core/ApiResult.ts b/www/app/api/core/ApiResult.ts deleted file mode 100644 index 05040ba8..00000000 --- a/www/app/api/core/ApiResult.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type ApiResult = { - readonly body: TData; - readonly ok: boolean; - readonly status: number; - readonly statusText: string; - readonly url: string; -}; diff --git a/www/app/api/core/AxiosHttpRequest.ts b/www/app/api/core/AxiosHttpRequest.ts deleted file mode 100644 index aba5096e..00000000 --- a/www/app/api/core/AxiosHttpRequest.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { ApiRequestOptions } from "./ApiRequestOptions"; -import { BaseHttpRequest } from "./BaseHttpRequest"; -import type { CancelablePromise } from "./CancelablePromise"; -import type { OpenAPIConfig } from "./OpenAPI"; -import { request as __request } from "./request"; - -export class AxiosHttpRequest extends BaseHttpRequest { - constructor(config: OpenAPIConfig) { - super(config); - } - - /** - * Request method - * @param options The request options from the service - * @returns CancelablePromise - * @throws ApiError - */ - public override request( - options: ApiRequestOptions, - ): CancelablePromise { - return __request(this.config, options); - } -} diff --git a/www/app/api/core/BaseHttpRequest.ts b/www/app/api/core/BaseHttpRequest.ts deleted file mode 100644 index 3f89861c..00000000 --- a/www/app/api/core/BaseHttpRequest.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { ApiRequestOptions } from "./ApiRequestOptions"; -import type { CancelablePromise } from "./CancelablePromise"; -import type { OpenAPIConfig } from "./OpenAPI"; - -export abstract class BaseHttpRequest { - constructor(public readonly config: OpenAPIConfig) {} - - public abstract request( - options: ApiRequestOptions, - ): CancelablePromise; -} diff --git a/www/app/api/core/CancelablePromise.ts b/www/app/api/core/CancelablePromise.ts deleted file mode 100644 index 0640e989..00000000 --- a/www/app/api/core/CancelablePromise.ts +++ /dev/null @@ -1,126 +0,0 @@ -export class CancelError extends Error { - constructor(message: string) { - super(message); - this.name = "CancelError"; - } - - public get isCancelled(): boolean { - return true; - } -} - -export interface OnCancel { - readonly isResolved: boolean; - readonly isRejected: boolean; - readonly isCancelled: boolean; - - (cancelHandler: () => void): void; -} - -export class CancelablePromise implements Promise { - private _isResolved: boolean; - private _isRejected: boolean; - private _isCancelled: boolean; - readonly cancelHandlers: (() => void)[]; - readonly promise: Promise; - private _resolve?: (value: T | PromiseLike) => void; - private _reject?: (reason?: unknown) => void; - - constructor( - executor: ( - resolve: (value: T | PromiseLike) => void, - reject: (reason?: unknown) => void, - onCancel: OnCancel, - ) => void, - ) { - this._isResolved = false; - this._isRejected = false; - this._isCancelled = false; - this.cancelHandlers = []; - this.promise = new Promise((resolve, reject) => { - this._resolve = resolve; - this._reject = reject; - - const onResolve = (value: T | PromiseLike): void => { - if (this._isResolved || this._isRejected || this._isCancelled) { - return; - } - this._isResolved = true; - if (this._resolve) this._resolve(value); - }; - - const onReject = (reason?: unknown): void => { - if (this._isResolved || this._isRejected || this._isCancelled) { - return; - } - this._isRejected = true; - if (this._reject) this._reject(reason); - }; - - const onCancel = (cancelHandler: () => void): void => { - if (this._isResolved || this._isRejected || this._isCancelled) { - return; - } - this.cancelHandlers.push(cancelHandler); - }; - - Object.defineProperty(onCancel, "isResolved", { - get: (): boolean => this._isResolved, - }); - - Object.defineProperty(onCancel, "isRejected", { - get: (): boolean => this._isRejected, - }); - - Object.defineProperty(onCancel, "isCancelled", { - get: (): boolean => this._isCancelled, - }); - - return executor(onResolve, onReject, onCancel as OnCancel); - }); - } - - get [Symbol.toStringTag]() { - return "Cancellable Promise"; - } - - public then( - onFulfilled?: ((value: T) => TResult1 | PromiseLike) | null, - onRejected?: ((reason: unknown) => TResult2 | PromiseLike) | null, - ): Promise { - return this.promise.then(onFulfilled, onRejected); - } - - public catch( - onRejected?: ((reason: unknown) => TResult | PromiseLike) | null, - ): Promise { - return this.promise.catch(onRejected); - } - - public finally(onFinally?: (() => void) | null): Promise { - return this.promise.finally(onFinally); - } - - public cancel(): void { - if (this._isResolved || this._isRejected || this._isCancelled) { - return; - } - this._isCancelled = true; - if (this.cancelHandlers.length) { - try { - for (const cancelHandler of this.cancelHandlers) { - cancelHandler(); - } - } catch (error) { - console.warn("Cancellation threw an error", error); - return; - } - } - this.cancelHandlers.length = 0; - if (this._reject) this._reject(new CancelError("Request aborted")); - } - - public get isCancelled(): boolean { - return this._isCancelled; - } -} diff --git a/www/app/api/core/OpenAPI.ts b/www/app/api/core/OpenAPI.ts deleted file mode 100644 index 20ea0ed9..00000000 --- a/www/app/api/core/OpenAPI.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { AxiosRequestConfig, AxiosResponse } from "axios"; -import type { ApiRequestOptions } from "./ApiRequestOptions"; - -type Headers = Record; -type Middleware = (value: T) => T | Promise; -type Resolver = (options: ApiRequestOptions) => Promise; - -export class Interceptors { - _fns: Middleware[]; - - constructor() { - this._fns = []; - } - - eject(fn: Middleware): void { - const index = this._fns.indexOf(fn); - if (index !== -1) { - this._fns = [...this._fns.slice(0, index), ...this._fns.slice(index + 1)]; - } - } - - use(fn: Middleware): void { - this._fns = [...this._fns, fn]; - } -} - -export type OpenAPIConfig = { - BASE: string; - CREDENTIALS: "include" | "omit" | "same-origin"; - ENCODE_PATH?: ((path: string) => string) | undefined; - HEADERS?: Headers | Resolver | undefined; - PASSWORD?: string | Resolver | undefined; - TOKEN?: string | Resolver | undefined; - USERNAME?: string | Resolver | undefined; - VERSION: string; - WITH_CREDENTIALS: boolean; - interceptors: { - request: Interceptors; - response: Interceptors; - }; -}; - -export const OpenAPI: OpenAPIConfig = { - BASE: "", - CREDENTIALS: "include", - ENCODE_PATH: undefined, - HEADERS: undefined, - PASSWORD: undefined, - TOKEN: undefined, - USERNAME: undefined, - VERSION: "0.1.0", - WITH_CREDENTIALS: false, - interceptors: { - request: new Interceptors(), - response: new Interceptors(), - }, -}; diff --git a/www/app/api/core/request.ts b/www/app/api/core/request.ts deleted file mode 100644 index b576207e..00000000 --- a/www/app/api/core/request.ts +++ /dev/null @@ -1,387 +0,0 @@ -import axios from "axios"; -import type { - AxiosError, - AxiosRequestConfig, - AxiosResponse, - AxiosInstance, -} from "axios"; - -import { ApiError } from "./ApiError"; -import type { ApiRequestOptions } from "./ApiRequestOptions"; -import type { ApiResult } from "./ApiResult"; -import { CancelablePromise } from "./CancelablePromise"; -import type { OnCancel } from "./CancelablePromise"; -import type { OpenAPIConfig } from "./OpenAPI"; - -export const isString = (value: unknown): value is string => { - return typeof value === "string"; -}; - -export const isStringWithValue = (value: unknown): value is string => { - return isString(value) && value !== ""; -}; - -export const isBlob = (value: any): value is Blob => { - return value instanceof Blob; -}; - -export const isFormData = (value: unknown): value is FormData => { - return value instanceof FormData; -}; - -export const isSuccess = (status: number): boolean => { - return status >= 200 && status < 300; -}; - -export const base64 = (str: string): string => { - try { - return btoa(str); - } catch (err) { - // @ts-ignore - return Buffer.from(str).toString("base64"); - } -}; - -export const getQueryString = (params: Record): string => { - const qs: string[] = []; - - const append = (key: string, value: unknown) => { - qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); - }; - - const encodePair = (key: string, value: unknown) => { - if (value === undefined || value === null) { - return; - } - - if (value instanceof Date) { - append(key, value.toISOString()); - } else if (Array.isArray(value)) { - value.forEach((v) => encodePair(key, v)); - } else if (typeof value === "object") { - Object.entries(value).forEach(([k, v]) => encodePair(`${key}[${k}]`, v)); - } else { - append(key, value); - } - }; - - Object.entries(params).forEach(([key, value]) => encodePair(key, value)); - - return qs.length ? `?${qs.join("&")}` : ""; -}; - -const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => { - const encoder = config.ENCODE_PATH || encodeURI; - - const path = options.url - .replace("{api-version}", config.VERSION) - .replace(/{(.*?)}/g, (substring: string, group: string) => { - if (options.path?.hasOwnProperty(group)) { - return encoder(String(options.path[group])); - } - return substring; - }); - - const url = config.BASE + path; - return options.query ? url + getQueryString(options.query) : url; -}; - -export const getFormData = ( - options: ApiRequestOptions, -): FormData | undefined => { - if (options.formData) { - const formData = new FormData(); - - const process = (key: string, value: unknown) => { - if (isString(value) || isBlob(value)) { - formData.append(key, value); - } else { - formData.append(key, JSON.stringify(value)); - } - }; - - Object.entries(options.formData) - .filter(([, value]) => value !== undefined && value !== null) - .forEach(([key, value]) => { - if (Array.isArray(value)) { - value.forEach((v) => process(key, v)); - } else { - process(key, value); - } - }); - - return formData; - } - return undefined; -}; - -type Resolver = (options: ApiRequestOptions) => Promise; - -export const resolve = async ( - options: ApiRequestOptions, - resolver?: T | Resolver, -): Promise => { - if (typeof resolver === "function") { - return (resolver as Resolver)(options); - } - return resolver; -}; - -export const getHeaders = async ( - config: OpenAPIConfig, - options: ApiRequestOptions, -): Promise> => { - const [token, username, password, additionalHeaders] = await Promise.all([ - // @ts-ignore - resolve(options, config.TOKEN), - // @ts-ignore - resolve(options, config.USERNAME), - // @ts-ignore - resolve(options, config.PASSWORD), - // @ts-ignore - resolve(options, config.HEADERS), - ]); - - const headers = Object.entries({ - Accept: "application/json", - ...additionalHeaders, - ...options.headers, - }) - .filter(([, value]) => value !== undefined && value !== null) - .reduce( - (headers, [key, value]) => ({ - ...headers, - [key]: String(value), - }), - {} as Record, - ); - - if (isStringWithValue(token)) { - headers["Authorization"] = `Bearer ${token}`; - } - - if (isStringWithValue(username) && isStringWithValue(password)) { - const credentials = base64(`${username}:${password}`); - headers["Authorization"] = `Basic ${credentials}`; - } - - if (options.body !== undefined) { - if (options.mediaType) { - headers["Content-Type"] = options.mediaType; - } else if (isBlob(options.body)) { - headers["Content-Type"] = options.body.type || "application/octet-stream"; - } else if (isString(options.body)) { - headers["Content-Type"] = "text/plain"; - } else if (!isFormData(options.body)) { - headers["Content-Type"] = "application/json"; - } - } else if (options.formData !== undefined) { - if (options.mediaType) { - headers["Content-Type"] = options.mediaType; - } - } - - return headers; -}; - -export const getRequestBody = (options: ApiRequestOptions): unknown => { - if (options.body) { - return options.body; - } - return undefined; -}; - -export const sendRequest = async ( - config: OpenAPIConfig, - options: ApiRequestOptions, - url: string, - body: unknown, - formData: FormData | undefined, - headers: Record, - onCancel: OnCancel, - axiosClient: AxiosInstance, -): Promise> => { - const controller = new AbortController(); - - let requestConfig: AxiosRequestConfig = { - data: body ?? formData, - headers, - method: options.method, - signal: controller.signal, - url, - withCredentials: config.WITH_CREDENTIALS, - }; - - onCancel(() => controller.abort()); - - for (const fn of config.interceptors.request._fns) { - requestConfig = await fn(requestConfig); - } - - try { - return await axiosClient.request(requestConfig); - } catch (error) { - const axiosError = error as AxiosError; - if (axiosError.response) { - return axiosError.response; - } - throw error; - } -}; - -export const getResponseHeader = ( - response: AxiosResponse, - responseHeader?: string, -): string | undefined => { - if (responseHeader) { - const content = response.headers[responseHeader]; - if (isString(content)) { - return content; - } - } - return undefined; -}; - -export const getResponseBody = (response: AxiosResponse): unknown => { - if (response.status !== 204) { - return response.data; - } - return undefined; -}; - -export const catchErrorCodes = ( - options: ApiRequestOptions, - result: ApiResult, -): void => { - const errors: Record = { - 400: "Bad Request", - 401: "Unauthorized", - 402: "Payment Required", - 403: "Forbidden", - 404: "Not Found", - 405: "Method Not Allowed", - 406: "Not Acceptable", - 407: "Proxy Authentication Required", - 408: "Request Timeout", - 409: "Conflict", - 410: "Gone", - 411: "Length Required", - 412: "Precondition Failed", - 413: "Payload Too Large", - 414: "URI Too Long", - 415: "Unsupported Media Type", - 416: "Range Not Satisfiable", - 417: "Expectation Failed", - 418: "Im a teapot", - 421: "Misdirected Request", - 422: "Unprocessable Content", - 423: "Locked", - 424: "Failed Dependency", - 425: "Too Early", - 426: "Upgrade Required", - 428: "Precondition Required", - 429: "Too Many Requests", - 431: "Request Header Fields Too Large", - 451: "Unavailable For Legal Reasons", - 500: "Internal Server Error", - 501: "Not Implemented", - 502: "Bad Gateway", - 503: "Service Unavailable", - 504: "Gateway Timeout", - 505: "HTTP Version Not Supported", - 506: "Variant Also Negotiates", - 507: "Insufficient Storage", - 508: "Loop Detected", - 510: "Not Extended", - 511: "Network Authentication Required", - ...options.errors, - }; - - const error = errors[result.status]; - if (error) { - throw new ApiError(options, result, error); - } - - if (!result.ok) { - const errorStatus = result.status ?? "unknown"; - const errorStatusText = result.statusText ?? "unknown"; - const errorBody = (() => { - try { - return JSON.stringify(result.body, null, 2); - } catch (e) { - return undefined; - } - })(); - - throw new ApiError( - options, - result, - `Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`, - ); - } -}; - -/** - * Request method - * @param config The OpenAPI configuration object - * @param options The request options from the service - * @param axiosClient The axios client instance to use - * @returns CancelablePromise - * @throws ApiError - */ -export const request = ( - config: OpenAPIConfig, - options: ApiRequestOptions, - axiosClient: AxiosInstance = axios, -): CancelablePromise => { - return new CancelablePromise(async (resolve, reject, onCancel) => { - try { - const url = getUrl(config, options); - const formData = getFormData(options); - const body = getRequestBody(options); - const headers = await getHeaders(config, options); - - if (!onCancel.isCancelled) { - let response = await sendRequest( - config, - options, - url, - body, - formData, - headers, - onCancel, - axiosClient, - ); - - for (const fn of config.interceptors.response._fns) { - response = await fn(response); - } - - const responseBody = getResponseBody(response); - const responseHeader = getResponseHeader( - response, - options.responseHeader, - ); - - let transformedBody = responseBody; - if (options.responseTransformer && isSuccess(response.status)) { - transformedBody = await options.responseTransformer(responseBody); - } - - const result: ApiResult = { - url, - ok: isSuccess(response.status), - status: response.status, - statusText: response.statusText, - body: responseHeader ?? transformedBody, - }; - - catchErrorCodes(options, result); - - resolve(result.body); - } - } catch (error) { - reject(error); - } - }); -}; diff --git a/www/app/api/index.ts b/www/app/api/index.ts deleted file mode 100644 index 27fbb57d..00000000 --- a/www/app/api/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts -export { OpenApi } from "./OpenApi"; -export { ApiError } from "./core/ApiError"; -export { BaseHttpRequest } from "./core/BaseHttpRequest"; -export { CancelablePromise, CancelError } from "./core/CancelablePromise"; -export { OpenAPI, type OpenAPIConfig } from "./core/OpenAPI"; -export * from "./schemas.gen"; -export * from "./services.gen"; -export * from "./types.gen"; diff --git a/www/app/api/schemas.gen.ts b/www/app/api/schemas.gen.ts index 03091a5f..e69de29b 100644 --- a/www/app/api/schemas.gen.ts +++ b/www/app/api/schemas.gen.ts @@ -1,1776 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export const $AudioWaveform = { - properties: { - data: { - items: { - type: "number", - }, - type: "array", - title: "Data", - }, - }, - type: "object", - required: ["data"], - title: "AudioWaveform", -} as const; - -export const $Body_transcript_record_upload_v1_transcripts__transcript_id__record_upload_post = - { - properties: { - chunk: { - type: "string", - format: "binary", - title: "Chunk", - }, - }, - type: "object", - required: ["chunk"], - title: - "Body_transcript_record_upload_v1_transcripts__transcript_id__record_upload_post", - } as const; - -export const $CreateParticipant = { - properties: { - speaker: { - anyOf: [ - { - type: "integer", - }, - { - type: "null", - }, - ], - title: "Speaker", - }, - name: { - type: "string", - title: "Name", - }, - }, - type: "object", - required: ["name"], - title: "CreateParticipant", -} as const; - -export const $CreateRoom = { - properties: { - name: { - type: "string", - title: "Name", - }, - zulip_auto_post: { - type: "boolean", - title: "Zulip Auto Post", - }, - zulip_stream: { - type: "string", - title: "Zulip Stream", - }, - zulip_topic: { - type: "string", - title: "Zulip Topic", - }, - is_locked: { - type: "boolean", - title: "Is Locked", - }, - room_mode: { - type: "string", - title: "Room Mode", - }, - recording_type: { - type: "string", - title: "Recording Type", - }, - recording_trigger: { - type: "string", - title: "Recording Trigger", - }, - is_shared: { - type: "boolean", - title: "Is Shared", - }, - webhook_url: { - type: "string", - title: "Webhook Url", - }, - webhook_secret: { - type: "string", - title: "Webhook Secret", - }, - }, - type: "object", - required: [ - "name", - "zulip_auto_post", - "zulip_stream", - "zulip_topic", - "is_locked", - "room_mode", - "recording_type", - "recording_trigger", - "is_shared", - "webhook_url", - "webhook_secret", - ], - title: "CreateRoom", -} as const; - -export const $CreateTranscript = { - properties: { - name: { - type: "string", - title: "Name", - }, - source_language: { - type: "string", - title: "Source Language", - default: "en", - }, - target_language: { - type: "string", - title: "Target Language", - default: "en", - }, - source_kind: { - anyOf: [ - { - $ref: "#/components/schemas/SourceKind", - }, - { - type: "null", - }, - ], - }, - }, - type: "object", - required: ["name"], - title: "CreateTranscript", -} as const; - -export const $DeletionStatus = { - properties: { - status: { - type: "string", - title: "Status", - }, - }, - type: "object", - required: ["status"], - title: "DeletionStatus", -} as const; - -export const $GetTranscript = { - properties: { - id: { - type: "string", - title: "Id", - }, - user_id: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "User Id", - }, - name: { - type: "string", - title: "Name", - }, - status: { - type: "string", - title: "Status", - }, - locked: { - type: "boolean", - title: "Locked", - }, - duration: { - type: "number", - title: "Duration", - }, - title: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Title", - }, - short_summary: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Short Summary", - }, - long_summary: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Long Summary", - }, - created_at: { - type: "string", - title: "Created At", - }, - share_mode: { - type: "string", - title: "Share Mode", - default: "private", - }, - source_language: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Source Language", - }, - target_language: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Target Language", - }, - reviewed: { - type: "boolean", - title: "Reviewed", - }, - meeting_id: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Meeting Id", - }, - source_kind: { - $ref: "#/components/schemas/SourceKind", - }, - room_id: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Room Id", - }, - room_name: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Room Name", - }, - audio_deleted: { - anyOf: [ - { - type: "boolean", - }, - { - type: "null", - }, - ], - title: "Audio Deleted", - }, - participants: { - anyOf: [ - { - items: { - $ref: "#/components/schemas/TranscriptParticipant", - }, - type: "array", - }, - { - type: "null", - }, - ], - title: "Participants", - }, - }, - type: "object", - required: [ - "id", - "user_id", - "name", - "status", - "locked", - "duration", - "title", - "short_summary", - "long_summary", - "created_at", - "source_language", - "target_language", - "reviewed", - "meeting_id", - "source_kind", - "participants", - ], - title: "GetTranscript", -} as const; - -export const $GetTranscriptMinimal = { - properties: { - id: { - type: "string", - title: "Id", - }, - user_id: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "User Id", - }, - name: { - type: "string", - title: "Name", - }, - status: { - type: "string", - title: "Status", - }, - locked: { - type: "boolean", - title: "Locked", - }, - duration: { - type: "number", - title: "Duration", - }, - title: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Title", - }, - short_summary: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Short Summary", - }, - long_summary: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Long Summary", - }, - created_at: { - type: "string", - title: "Created At", - }, - share_mode: { - type: "string", - title: "Share Mode", - default: "private", - }, - source_language: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Source Language", - }, - target_language: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Target Language", - }, - reviewed: { - type: "boolean", - title: "Reviewed", - }, - meeting_id: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Meeting Id", - }, - source_kind: { - $ref: "#/components/schemas/SourceKind", - }, - room_id: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Room Id", - }, - room_name: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Room Name", - }, - audio_deleted: { - anyOf: [ - { - type: "boolean", - }, - { - type: "null", - }, - ], - title: "Audio Deleted", - }, - }, - type: "object", - required: [ - "id", - "user_id", - "name", - "status", - "locked", - "duration", - "title", - "short_summary", - "long_summary", - "created_at", - "source_language", - "target_language", - "reviewed", - "meeting_id", - "source_kind", - ], - title: "GetTranscriptMinimal", -} as const; - -export const $GetTranscriptSegmentTopic = { - properties: { - text: { - type: "string", - title: "Text", - }, - start: { - type: "number", - title: "Start", - }, - speaker: { - type: "integer", - title: "Speaker", - }, - }, - type: "object", - required: ["text", "start", "speaker"], - title: "GetTranscriptSegmentTopic", -} as const; - -export const $GetTranscriptTopic = { - properties: { - id: { - type: "string", - title: "Id", - }, - title: { - type: "string", - title: "Title", - }, - summary: { - type: "string", - title: "Summary", - }, - timestamp: { - type: "number", - title: "Timestamp", - }, - duration: { - anyOf: [ - { - type: "number", - }, - { - type: "null", - }, - ], - title: "Duration", - }, - transcript: { - type: "string", - title: "Transcript", - }, - segments: { - items: { - $ref: "#/components/schemas/GetTranscriptSegmentTopic", - }, - type: "array", - title: "Segments", - default: [], - }, - }, - type: "object", - required: ["id", "title", "summary", "timestamp", "duration", "transcript"], - title: "GetTranscriptTopic", -} as const; - -export const $GetTranscriptTopicWithWords = { - properties: { - id: { - type: "string", - title: "Id", - }, - title: { - type: "string", - title: "Title", - }, - summary: { - type: "string", - title: "Summary", - }, - timestamp: { - type: "number", - title: "Timestamp", - }, - duration: { - anyOf: [ - { - type: "number", - }, - { - type: "null", - }, - ], - title: "Duration", - }, - transcript: { - type: "string", - title: "Transcript", - }, - segments: { - items: { - $ref: "#/components/schemas/GetTranscriptSegmentTopic", - }, - type: "array", - title: "Segments", - default: [], - }, - words: { - items: { - $ref: "#/components/schemas/Word", - }, - type: "array", - title: "Words", - default: [], - }, - }, - type: "object", - required: ["id", "title", "summary", "timestamp", "duration", "transcript"], - title: "GetTranscriptTopicWithWords", -} as const; - -export const $GetTranscriptTopicWithWordsPerSpeaker = { - properties: { - id: { - type: "string", - title: "Id", - }, - title: { - type: "string", - title: "Title", - }, - summary: { - type: "string", - title: "Summary", - }, - timestamp: { - type: "number", - title: "Timestamp", - }, - duration: { - anyOf: [ - { - type: "number", - }, - { - type: "null", - }, - ], - title: "Duration", - }, - transcript: { - type: "string", - title: "Transcript", - }, - segments: { - items: { - $ref: "#/components/schemas/GetTranscriptSegmentTopic", - }, - type: "array", - title: "Segments", - default: [], - }, - words_per_speaker: { - items: { - $ref: "#/components/schemas/SpeakerWords", - }, - type: "array", - title: "Words Per Speaker", - default: [], - }, - }, - type: "object", - required: ["id", "title", "summary", "timestamp", "duration", "transcript"], - title: "GetTranscriptTopicWithWordsPerSpeaker", -} as const; - -export const $HTTPValidationError = { - properties: { - detail: { - items: { - $ref: "#/components/schemas/ValidationError", - }, - type: "array", - title: "Detail", - }, - }, - type: "object", - title: "HTTPValidationError", -} as const; - -export const $Meeting = { - properties: { - id: { - type: "string", - title: "Id", - }, - room_name: { - type: "string", - title: "Room Name", - }, - room_url: { - type: "string", - title: "Room Url", - }, - host_room_url: { - type: "string", - title: "Host Room Url", - }, - start_date: { - type: "string", - format: "date-time", - title: "Start Date", - }, - end_date: { - type: "string", - format: "date-time", - title: "End Date", - }, - recording_type: { - type: "string", - enum: ["none", "local", "cloud"], - title: "Recording Type", - default: "cloud", - }, - }, - type: "object", - required: [ - "id", - "room_name", - "room_url", - "host_room_url", - "start_date", - "end_date", - ], - title: "Meeting", -} as const; - -export const $MeetingConsentRequest = { - properties: { - consent_given: { - type: "boolean", - title: "Consent Given", - }, - }, - type: "object", - required: ["consent_given"], - title: "MeetingConsentRequest", -} as const; - -export const $Page_GetTranscriptMinimal_ = { - properties: { - items: { - items: { - $ref: "#/components/schemas/GetTranscriptMinimal", - }, - type: "array", - title: "Items", - }, - total: { - anyOf: [ - { - type: "integer", - minimum: 0, - }, - { - type: "null", - }, - ], - title: "Total", - }, - page: { - anyOf: [ - { - type: "integer", - minimum: 1, - }, - { - type: "null", - }, - ], - title: "Page", - }, - size: { - anyOf: [ - { - type: "integer", - minimum: 1, - }, - { - type: "null", - }, - ], - title: "Size", - }, - pages: { - anyOf: [ - { - type: "integer", - minimum: 0, - }, - { - type: "null", - }, - ], - title: "Pages", - }, - }, - type: "object", - required: ["items", "page", "size"], - title: "Page[GetTranscriptMinimal]", -} as const; - -export const $Page_RoomDetails_ = { - properties: { - items: { - items: { - $ref: "#/components/schemas/RoomDetails", - }, - type: "array", - title: "Items", - }, - total: { - anyOf: [ - { - type: "integer", - minimum: 0, - }, - { - type: "null", - }, - ], - title: "Total", - }, - page: { - anyOf: [ - { - type: "integer", - minimum: 1, - }, - { - type: "null", - }, - ], - title: "Page", - }, - size: { - anyOf: [ - { - type: "integer", - minimum: 1, - }, - { - type: "null", - }, - ], - title: "Size", - }, - pages: { - anyOf: [ - { - type: "integer", - minimum: 0, - }, - { - type: "null", - }, - ], - title: "Pages", - }, - }, - type: "object", - required: ["items", "page", "size"], - title: "Page[RoomDetails]", -} as const; - -export const $Participant = { - properties: { - id: { - type: "string", - title: "Id", - }, - speaker: { - anyOf: [ - { - type: "integer", - }, - { - type: "null", - }, - ], - title: "Speaker", - }, - name: { - type: "string", - title: "Name", - }, - }, - type: "object", - required: ["id", "speaker", "name"], - title: "Participant", -} as const; - -export const $Room = { - properties: { - id: { - type: "string", - title: "Id", - }, - name: { - type: "string", - title: "Name", - }, - user_id: { - type: "string", - title: "User Id", - }, - created_at: { - type: "string", - format: "date-time", - title: "Created At", - }, - zulip_auto_post: { - type: "boolean", - title: "Zulip Auto Post", - }, - zulip_stream: { - type: "string", - title: "Zulip Stream", - }, - zulip_topic: { - type: "string", - title: "Zulip Topic", - }, - is_locked: { - type: "boolean", - title: "Is Locked", - }, - room_mode: { - type: "string", - title: "Room Mode", - }, - recording_type: { - type: "string", - title: "Recording Type", - }, - recording_trigger: { - type: "string", - title: "Recording Trigger", - }, - is_shared: { - type: "boolean", - title: "Is Shared", - }, - }, - type: "object", - required: [ - "id", - "name", - "user_id", - "created_at", - "zulip_auto_post", - "zulip_stream", - "zulip_topic", - "is_locked", - "room_mode", - "recording_type", - "recording_trigger", - "is_shared", - ], - title: "Room", -} as const; - -export const $RoomDetails = { - properties: { - id: { - type: "string", - title: "Id", - }, - name: { - type: "string", - title: "Name", - }, - user_id: { - type: "string", - title: "User Id", - }, - created_at: { - type: "string", - format: "date-time", - title: "Created At", - }, - zulip_auto_post: { - type: "boolean", - title: "Zulip Auto Post", - }, - zulip_stream: { - type: "string", - title: "Zulip Stream", - }, - zulip_topic: { - type: "string", - title: "Zulip Topic", - }, - is_locked: { - type: "boolean", - title: "Is Locked", - }, - room_mode: { - type: "string", - title: "Room Mode", - }, - recording_type: { - type: "string", - title: "Recording Type", - }, - recording_trigger: { - type: "string", - title: "Recording Trigger", - }, - is_shared: { - type: "boolean", - title: "Is Shared", - }, - webhook_url: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Webhook Url", - }, - webhook_secret: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Webhook Secret", - }, - }, - type: "object", - required: [ - "id", - "name", - "user_id", - "created_at", - "zulip_auto_post", - "zulip_stream", - "zulip_topic", - "is_locked", - "room_mode", - "recording_type", - "recording_trigger", - "is_shared", - "webhook_url", - "webhook_secret", - ], - title: "RoomDetails", -} as const; - -export const $RtcOffer = { - properties: { - sdp: { - type: "string", - title: "Sdp", - }, - type: { - type: "string", - title: "Type", - }, - }, - type: "object", - required: ["sdp", "type"], - title: "RtcOffer", -} as const; - -export const $SearchResponse = { - properties: { - results: { - items: { - $ref: "#/components/schemas/SearchResult", - }, - type: "array", - title: "Results", - }, - total: { - type: "integer", - minimum: 0, - title: "Total", - description: "Total number of search results", - }, - query: { - anyOf: [ - { - type: "string", - minLength: 1, - description: "Search query text", - }, - { - type: "null", - }, - ], - title: "Query", - }, - limit: { - type: "integer", - maximum: 100, - minimum: 1, - title: "Limit", - description: "Results per page", - }, - offset: { - type: "integer", - minimum: 0, - title: "Offset", - description: "Number of results to skip", - }, - }, - type: "object", - required: ["results", "total", "limit", "offset"], - title: "SearchResponse", -} as const; - -export const $SearchResult = { - properties: { - id: { - type: "string", - minLength: 1, - title: "Id", - }, - title: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Title", - }, - user_id: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "User Id", - }, - room_id: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Room Id", - }, - room_name: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Room Name", - }, - source_kind: { - $ref: "#/components/schemas/SourceKind", - }, - created_at: { - type: "string", - title: "Created At", - }, - status: { - type: "string", - minLength: 1, - title: "Status", - }, - rank: { - type: "number", - maximum: 1, - minimum: 0, - title: "Rank", - }, - duration: { - anyOf: [ - { - type: "number", - minimum: 0, - }, - { - type: "null", - }, - ], - title: "Duration", - description: "Duration in seconds", - }, - search_snippets: { - items: { - type: "string", - }, - type: "array", - title: "Search Snippets", - description: "Text snippets around search matches", - }, - total_match_count: { - type: "integer", - minimum: 0, - title: "Total Match Count", - description: "Total number of matches found in the transcript", - default: 0, - }, - }, - type: "object", - required: [ - "id", - "source_kind", - "created_at", - "status", - "rank", - "duration", - "search_snippets", - ], - title: "SearchResult", - description: "Public search result model with computed fields.", -} as const; - -export const $SourceKind = { - type: "string", - enum: ["room", "live", "file"], - title: "SourceKind", -} as const; - -export const $SpeakerAssignment = { - properties: { - speaker: { - anyOf: [ - { - type: "integer", - minimum: 0, - }, - { - type: "null", - }, - ], - title: "Speaker", - }, - participant: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Participant", - }, - timestamp_from: { - type: "number", - title: "Timestamp From", - }, - timestamp_to: { - type: "number", - title: "Timestamp To", - }, - }, - type: "object", - required: ["timestamp_from", "timestamp_to"], - title: "SpeakerAssignment", -} as const; - -export const $SpeakerAssignmentStatus = { - properties: { - status: { - type: "string", - title: "Status", - }, - }, - type: "object", - required: ["status"], - title: "SpeakerAssignmentStatus", -} as const; - -export const $SpeakerMerge = { - properties: { - speaker_from: { - type: "integer", - title: "Speaker From", - }, - speaker_to: { - type: "integer", - title: "Speaker To", - }, - }, - type: "object", - required: ["speaker_from", "speaker_to"], - title: "SpeakerMerge", -} as const; - -export const $SpeakerWords = { - properties: { - speaker: { - type: "integer", - title: "Speaker", - }, - words: { - items: { - $ref: "#/components/schemas/Word", - }, - type: "array", - title: "Words", - }, - }, - type: "object", - required: ["speaker", "words"], - title: "SpeakerWords", -} as const; - -export const $Stream = { - properties: { - stream_id: { - type: "integer", - title: "Stream Id", - }, - name: { - type: "string", - title: "Name", - }, - }, - type: "object", - required: ["stream_id", "name"], - title: "Stream", -} as const; - -export const $Topic = { - properties: { - name: { - type: "string", - title: "Name", - }, - }, - type: "object", - required: ["name"], - title: "Topic", -} as const; - -export const $TranscriptParticipant = { - properties: { - id: { - type: "string", - title: "Id", - }, - speaker: { - anyOf: [ - { - type: "integer", - }, - { - type: "null", - }, - ], - title: "Speaker", - }, - name: { - type: "string", - title: "Name", - }, - }, - type: "object", - required: ["speaker", "name"], - title: "TranscriptParticipant", -} as const; - -export const $UpdateParticipant = { - properties: { - speaker: { - anyOf: [ - { - type: "integer", - }, - { - type: "null", - }, - ], - title: "Speaker", - }, - name: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Name", - }, - }, - type: "object", - title: "UpdateParticipant", -} as const; - -export const $UpdateRoom = { - properties: { - name: { - type: "string", - title: "Name", - }, - zulip_auto_post: { - type: "boolean", - title: "Zulip Auto Post", - }, - zulip_stream: { - type: "string", - title: "Zulip Stream", - }, - zulip_topic: { - type: "string", - title: "Zulip Topic", - }, - is_locked: { - type: "boolean", - title: "Is Locked", - }, - room_mode: { - type: "string", - title: "Room Mode", - }, - recording_type: { - type: "string", - title: "Recording Type", - }, - recording_trigger: { - type: "string", - title: "Recording Trigger", - }, - is_shared: { - type: "boolean", - title: "Is Shared", - }, - webhook_url: { - type: "string", - title: "Webhook Url", - }, - webhook_secret: { - type: "string", - title: "Webhook Secret", - }, - }, - type: "object", - required: [ - "name", - "zulip_auto_post", - "zulip_stream", - "zulip_topic", - "is_locked", - "room_mode", - "recording_type", - "recording_trigger", - "is_shared", - "webhook_url", - "webhook_secret", - ], - title: "UpdateRoom", -} as const; - -export const $UpdateTranscript = { - properties: { - name: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Name", - }, - locked: { - anyOf: [ - { - type: "boolean", - }, - { - type: "null", - }, - ], - title: "Locked", - }, - title: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Title", - }, - short_summary: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Short Summary", - }, - long_summary: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Long Summary", - }, - share_mode: { - anyOf: [ - { - type: "string", - enum: ["public", "semi-private", "private"], - }, - { - type: "null", - }, - ], - title: "Share Mode", - }, - participants: { - anyOf: [ - { - items: { - $ref: "#/components/schemas/TranscriptParticipant", - }, - type: "array", - }, - { - type: "null", - }, - ], - title: "Participants", - }, - reviewed: { - anyOf: [ - { - type: "boolean", - }, - { - type: "null", - }, - ], - title: "Reviewed", - }, - audio_deleted: { - anyOf: [ - { - type: "boolean", - }, - { - type: "null", - }, - ], - title: "Audio Deleted", - }, - }, - type: "object", - title: "UpdateTranscript", -} as const; - -export const $UserInfo = { - properties: { - sub: { - type: "string", - title: "Sub", - }, - email: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Email", - }, - email_verified: { - anyOf: [ - { - type: "boolean", - }, - { - type: "null", - }, - ], - title: "Email Verified", - }, - }, - type: "object", - required: ["sub", "email", "email_verified"], - title: "UserInfo", -} as const; - -export const $ValidationError = { - properties: { - loc: { - items: { - anyOf: [ - { - type: "string", - }, - { - type: "integer", - }, - ], - }, - type: "array", - title: "Location", - }, - msg: { - type: "string", - title: "Message", - }, - type: { - type: "string", - title: "Error Type", - }, - }, - type: "object", - required: ["loc", "msg", "type"], - title: "ValidationError", -} as const; - -export const $WebhookTestResult = { - properties: { - success: { - type: "boolean", - title: "Success", - }, - message: { - type: "string", - title: "Message", - default: "", - }, - error: { - type: "string", - title: "Error", - default: "", - }, - status_code: { - anyOf: [ - { - type: "integer", - }, - { - type: "null", - }, - ], - title: "Status Code", - }, - response_preview: { - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, - ], - title: "Response Preview", - }, - }, - type: "object", - required: ["success"], - title: "WebhookTestResult", -} as const; - -export const $WherebyWebhookEvent = { - properties: { - apiVersion: { - type: "string", - title: "Apiversion", - }, - id: { - type: "string", - title: "Id", - }, - createdAt: { - type: "string", - format: "date-time", - title: "Createdat", - }, - type: { - type: "string", - title: "Type", - }, - data: { - additionalProperties: true, - type: "object", - title: "Data", - }, - }, - type: "object", - required: ["apiVersion", "id", "createdAt", "type", "data"], - title: "WherebyWebhookEvent", -} as const; - -export const $Word = { - properties: { - text: { - type: "string", - title: "Text", - }, - start: { - type: "number", - minimum: 0, - title: "Start", - description: "Time in seconds with float part", - }, - end: { - type: "number", - minimum: 0, - title: "End", - description: "Time in seconds with float part", - }, - speaker: { - type: "integer", - title: "Speaker", - default: 0, - }, - }, - type: "object", - required: ["text", "start", "end"], - title: "Word", -} as const; diff --git a/www/app/api/services.gen.ts b/www/app/api/services.gen.ts index c9e027fb..e69de29b 100644 --- a/www/app/api/services.gen.ts +++ b/www/app/api/services.gen.ts @@ -1,942 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { CancelablePromise } from "./core/CancelablePromise"; -import type { BaseHttpRequest } from "./core/BaseHttpRequest"; -import type { - MetricsResponse, - V1MeetingAudioConsentData, - V1MeetingAudioConsentResponse, - V1RoomsListData, - V1RoomsListResponse, - V1RoomsCreateData, - V1RoomsCreateResponse, - V1RoomsGetData, - V1RoomsGetResponse, - V1RoomsUpdateData, - V1RoomsUpdateResponse, - V1RoomsDeleteData, - V1RoomsDeleteResponse, - V1RoomsCreateMeetingData, - V1RoomsCreateMeetingResponse, - V1RoomsTestWebhookData, - V1RoomsTestWebhookResponse, - V1TranscriptsListData, - V1TranscriptsListResponse, - V1TranscriptsCreateData, - V1TranscriptsCreateResponse, - V1TranscriptsSearchData, - V1TranscriptsSearchResponse, - V1TranscriptGetData, - V1TranscriptGetResponse, - V1TranscriptUpdateData, - V1TranscriptUpdateResponse, - V1TranscriptDeleteData, - V1TranscriptDeleteResponse, - V1TranscriptGetTopicsData, - V1TranscriptGetTopicsResponse, - V1TranscriptGetTopicsWithWordsData, - V1TranscriptGetTopicsWithWordsResponse, - V1TranscriptGetTopicsWithWordsPerSpeakerData, - V1TranscriptGetTopicsWithWordsPerSpeakerResponse, - V1TranscriptPostToZulipData, - V1TranscriptPostToZulipResponse, - V1TranscriptHeadAudioMp3Data, - V1TranscriptHeadAudioMp3Response, - V1TranscriptGetAudioMp3Data, - V1TranscriptGetAudioMp3Response, - V1TranscriptGetAudioWaveformData, - V1TranscriptGetAudioWaveformResponse, - V1TranscriptGetParticipantsData, - V1TranscriptGetParticipantsResponse, - V1TranscriptAddParticipantData, - V1TranscriptAddParticipantResponse, - V1TranscriptGetParticipantData, - V1TranscriptGetParticipantResponse, - V1TranscriptUpdateParticipantData, - V1TranscriptUpdateParticipantResponse, - V1TranscriptDeleteParticipantData, - V1TranscriptDeleteParticipantResponse, - V1TranscriptAssignSpeakerData, - V1TranscriptAssignSpeakerResponse, - V1TranscriptMergeSpeakerData, - V1TranscriptMergeSpeakerResponse, - V1TranscriptRecordUploadData, - V1TranscriptRecordUploadResponse, - V1TranscriptGetWebsocketEventsData, - V1TranscriptGetWebsocketEventsResponse, - V1TranscriptRecordWebrtcData, - V1TranscriptRecordWebrtcResponse, - V1TranscriptProcessData, - V1TranscriptProcessResponse, - V1UserMeResponse, - V1ZulipGetStreamsResponse, - V1ZulipGetTopicsData, - V1ZulipGetTopicsResponse, - V1WherebyWebhookData, - V1WherebyWebhookResponse, -} from "./types.gen"; - -export class DefaultService { - constructor(public readonly httpRequest: BaseHttpRequest) {} - - /** - * Metrics - * Endpoint that serves Prometheus metrics. - * @returns unknown Successful Response - * @throws ApiError - */ - public metrics(): CancelablePromise { - return this.httpRequest.request({ - method: "GET", - url: "/metrics", - }); - } - - /** - * Meeting Audio Consent - * @param data The data for the request. - * @param data.meetingId - * @param data.requestBody - * @returns unknown Successful Response - * @throws ApiError - */ - public v1MeetingAudioConsent( - data: V1MeetingAudioConsentData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "POST", - url: "/v1/meetings/{meeting_id}/consent", - path: { - meeting_id: data.meetingId, - }, - body: data.requestBody, - mediaType: "application/json", - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Rooms List - * @param data The data for the request. - * @param data.page Page number - * @param data.size Page size - * @returns Page_RoomDetails_ Successful Response - * @throws ApiError - */ - public v1RoomsList( - data: V1RoomsListData = {}, - ): CancelablePromise { - return this.httpRequest.request({ - method: "GET", - url: "/v1/rooms", - query: { - page: data.page, - size: data.size, - }, - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Rooms Create - * @param data The data for the request. - * @param data.requestBody - * @returns Room Successful Response - * @throws ApiError - */ - public v1RoomsCreate( - data: V1RoomsCreateData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "POST", - url: "/v1/rooms", - body: data.requestBody, - mediaType: "application/json", - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Rooms Get - * @param data The data for the request. - * @param data.roomId - * @returns RoomDetails Successful Response - * @throws ApiError - */ - public v1RoomsGet( - data: V1RoomsGetData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "GET", - url: "/v1/rooms/{room_id}", - path: { - room_id: data.roomId, - }, - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Rooms Update - * @param data The data for the request. - * @param data.roomId - * @param data.requestBody - * @returns RoomDetails Successful Response - * @throws ApiError - */ - public v1RoomsUpdate( - data: V1RoomsUpdateData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "PATCH", - url: "/v1/rooms/{room_id}", - path: { - room_id: data.roomId, - }, - body: data.requestBody, - mediaType: "application/json", - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Rooms Delete - * @param data The data for the request. - * @param data.roomId - * @returns DeletionStatus Successful Response - * @throws ApiError - */ - public v1RoomsDelete( - data: V1RoomsDeleteData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "DELETE", - url: "/v1/rooms/{room_id}", - path: { - room_id: data.roomId, - }, - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Rooms Create Meeting - * @param data The data for the request. - * @param data.roomName - * @returns Meeting Successful Response - * @throws ApiError - */ - public v1RoomsCreateMeeting( - data: V1RoomsCreateMeetingData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "POST", - url: "/v1/rooms/{room_name}/meeting", - path: { - room_name: data.roomName, - }, - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Rooms Test Webhook - * Test webhook configuration by sending a sample payload. - * @param data The data for the request. - * @param data.roomId - * @returns WebhookTestResult Successful Response - * @throws ApiError - */ - public v1RoomsTestWebhook( - data: V1RoomsTestWebhookData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "POST", - url: "/v1/rooms/{room_id}/webhook/test", - path: { - room_id: data.roomId, - }, - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Transcripts List - * @param data The data for the request. - * @param data.sourceKind - * @param data.roomId - * @param data.searchTerm - * @param data.page Page number - * @param data.size Page size - * @returns Page_GetTranscriptMinimal_ Successful Response - * @throws ApiError - */ - public v1TranscriptsList( - data: V1TranscriptsListData = {}, - ): CancelablePromise { - return this.httpRequest.request({ - method: "GET", - url: "/v1/transcripts", - query: { - source_kind: data.sourceKind, - room_id: data.roomId, - search_term: data.searchTerm, - page: data.page, - size: data.size, - }, - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Transcripts Create - * @param data The data for the request. - * @param data.requestBody - * @returns GetTranscript Successful Response - * @throws ApiError - */ - public v1TranscriptsCreate( - data: V1TranscriptsCreateData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "POST", - url: "/v1/transcripts", - body: data.requestBody, - mediaType: "application/json", - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Transcripts Search - * Full-text search across transcript titles and content. - * @param data The data for the request. - * @param data.q Search query text - * @param data.limit Results per page - * @param data.offset Number of results to skip - * @param data.roomId - * @param data.sourceKind - * @returns SearchResponse Successful Response - * @throws ApiError - */ - public v1TranscriptsSearch( - data: V1TranscriptsSearchData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "GET", - url: "/v1/transcripts/search", - query: { - q: data.q, - limit: data.limit, - offset: data.offset, - room_id: data.roomId, - source_kind: data.sourceKind, - }, - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Transcript Get - * @param data The data for the request. - * @param data.transcriptId - * @returns GetTranscript Successful Response - * @throws ApiError - */ - public v1TranscriptGet( - data: V1TranscriptGetData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "GET", - url: "/v1/transcripts/{transcript_id}", - path: { - transcript_id: data.transcriptId, - }, - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Transcript Update - * @param data The data for the request. - * @param data.transcriptId - * @param data.requestBody - * @returns GetTranscript Successful Response - * @throws ApiError - */ - public v1TranscriptUpdate( - data: V1TranscriptUpdateData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "PATCH", - url: "/v1/transcripts/{transcript_id}", - path: { - transcript_id: data.transcriptId, - }, - body: data.requestBody, - mediaType: "application/json", - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Transcript Delete - * @param data The data for the request. - * @param data.transcriptId - * @returns DeletionStatus Successful Response - * @throws ApiError - */ - public v1TranscriptDelete( - data: V1TranscriptDeleteData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "DELETE", - url: "/v1/transcripts/{transcript_id}", - path: { - transcript_id: data.transcriptId, - }, - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Transcript Get Topics - * @param data The data for the request. - * @param data.transcriptId - * @returns GetTranscriptTopic Successful Response - * @throws ApiError - */ - public v1TranscriptGetTopics( - data: V1TranscriptGetTopicsData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "GET", - url: "/v1/transcripts/{transcript_id}/topics", - path: { - transcript_id: data.transcriptId, - }, - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Transcript Get Topics With Words - * @param data The data for the request. - * @param data.transcriptId - * @returns GetTranscriptTopicWithWords Successful Response - * @throws ApiError - */ - public v1TranscriptGetTopicsWithWords( - data: V1TranscriptGetTopicsWithWordsData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "GET", - url: "/v1/transcripts/{transcript_id}/topics/with-words", - path: { - transcript_id: data.transcriptId, - }, - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Transcript Get Topics With Words Per Speaker - * @param data The data for the request. - * @param data.transcriptId - * @param data.topicId - * @returns GetTranscriptTopicWithWordsPerSpeaker Successful Response - * @throws ApiError - */ - public v1TranscriptGetTopicsWithWordsPerSpeaker( - data: V1TranscriptGetTopicsWithWordsPerSpeakerData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "GET", - url: "/v1/transcripts/{transcript_id}/topics/{topic_id}/words-per-speaker", - path: { - transcript_id: data.transcriptId, - topic_id: data.topicId, - }, - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Transcript Post To Zulip - * @param data The data for the request. - * @param data.transcriptId - * @param data.stream - * @param data.topic - * @param data.includeTopics - * @returns unknown Successful Response - * @throws ApiError - */ - public v1TranscriptPostToZulip( - data: V1TranscriptPostToZulipData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "POST", - url: "/v1/transcripts/{transcript_id}/zulip", - path: { - transcript_id: data.transcriptId, - }, - query: { - stream: data.stream, - topic: data.topic, - include_topics: data.includeTopics, - }, - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Transcript Get Audio Mp3 - * @param data The data for the request. - * @param data.transcriptId - * @param data.token - * @returns unknown Successful Response - * @throws ApiError - */ - public v1TranscriptHeadAudioMp3( - data: V1TranscriptHeadAudioMp3Data, - ): CancelablePromise { - return this.httpRequest.request({ - method: "HEAD", - url: "/v1/transcripts/{transcript_id}/audio/mp3", - path: { - transcript_id: data.transcriptId, - }, - query: { - token: data.token, - }, - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Transcript Get Audio Mp3 - * @param data The data for the request. - * @param data.transcriptId - * @param data.token - * @returns unknown Successful Response - * @throws ApiError - */ - public v1TranscriptGetAudioMp3( - data: V1TranscriptGetAudioMp3Data, - ): CancelablePromise { - return this.httpRequest.request({ - method: "GET", - url: "/v1/transcripts/{transcript_id}/audio/mp3", - path: { - transcript_id: data.transcriptId, - }, - query: { - token: data.token, - }, - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Transcript Get Audio Waveform - * @param data The data for the request. - * @param data.transcriptId - * @returns AudioWaveform Successful Response - * @throws ApiError - */ - public v1TranscriptGetAudioWaveform( - data: V1TranscriptGetAudioWaveformData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "GET", - url: "/v1/transcripts/{transcript_id}/audio/waveform", - path: { - transcript_id: data.transcriptId, - }, - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Transcript Get Participants - * @param data The data for the request. - * @param data.transcriptId - * @returns Participant Successful Response - * @throws ApiError - */ - public v1TranscriptGetParticipants( - data: V1TranscriptGetParticipantsData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "GET", - url: "/v1/transcripts/{transcript_id}/participants", - path: { - transcript_id: data.transcriptId, - }, - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Transcript Add Participant - * @param data The data for the request. - * @param data.transcriptId - * @param data.requestBody - * @returns Participant Successful Response - * @throws ApiError - */ - public v1TranscriptAddParticipant( - data: V1TranscriptAddParticipantData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "POST", - url: "/v1/transcripts/{transcript_id}/participants", - path: { - transcript_id: data.transcriptId, - }, - body: data.requestBody, - mediaType: "application/json", - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Transcript Get Participant - * @param data The data for the request. - * @param data.transcriptId - * @param data.participantId - * @returns Participant Successful Response - * @throws ApiError - */ - public v1TranscriptGetParticipant( - data: V1TranscriptGetParticipantData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "GET", - url: "/v1/transcripts/{transcript_id}/participants/{participant_id}", - path: { - transcript_id: data.transcriptId, - participant_id: data.participantId, - }, - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Transcript Update Participant - * @param data The data for the request. - * @param data.transcriptId - * @param data.participantId - * @param data.requestBody - * @returns Participant Successful Response - * @throws ApiError - */ - public v1TranscriptUpdateParticipant( - data: V1TranscriptUpdateParticipantData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "PATCH", - url: "/v1/transcripts/{transcript_id}/participants/{participant_id}", - path: { - transcript_id: data.transcriptId, - participant_id: data.participantId, - }, - body: data.requestBody, - mediaType: "application/json", - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Transcript Delete Participant - * @param data The data for the request. - * @param data.transcriptId - * @param data.participantId - * @returns DeletionStatus Successful Response - * @throws ApiError - */ - public v1TranscriptDeleteParticipant( - data: V1TranscriptDeleteParticipantData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "DELETE", - url: "/v1/transcripts/{transcript_id}/participants/{participant_id}", - path: { - transcript_id: data.transcriptId, - participant_id: data.participantId, - }, - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Transcript Assign Speaker - * @param data The data for the request. - * @param data.transcriptId - * @param data.requestBody - * @returns SpeakerAssignmentStatus Successful Response - * @throws ApiError - */ - public v1TranscriptAssignSpeaker( - data: V1TranscriptAssignSpeakerData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "PATCH", - url: "/v1/transcripts/{transcript_id}/speaker/assign", - path: { - transcript_id: data.transcriptId, - }, - body: data.requestBody, - mediaType: "application/json", - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Transcript Merge Speaker - * @param data The data for the request. - * @param data.transcriptId - * @param data.requestBody - * @returns SpeakerAssignmentStatus Successful Response - * @throws ApiError - */ - public v1TranscriptMergeSpeaker( - data: V1TranscriptMergeSpeakerData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "PATCH", - url: "/v1/transcripts/{transcript_id}/speaker/merge", - path: { - transcript_id: data.transcriptId, - }, - body: data.requestBody, - mediaType: "application/json", - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Transcript Record Upload - * @param data The data for the request. - * @param data.transcriptId - * @param data.chunkNumber - * @param data.totalChunks - * @param data.formData - * @returns unknown Successful Response - * @throws ApiError - */ - public v1TranscriptRecordUpload( - data: V1TranscriptRecordUploadData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "POST", - url: "/v1/transcripts/{transcript_id}/record/upload", - path: { - transcript_id: data.transcriptId, - }, - query: { - chunk_number: data.chunkNumber, - total_chunks: data.totalChunks, - }, - formData: data.formData, - mediaType: "multipart/form-data", - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Transcript Get Websocket Events - * @param data The data for the request. - * @param data.transcriptId - * @returns unknown Successful Response - * @throws ApiError - */ - public v1TranscriptGetWebsocketEvents( - data: V1TranscriptGetWebsocketEventsData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "GET", - url: "/v1/transcripts/{transcript_id}/events", - path: { - transcript_id: data.transcriptId, - }, - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Transcript Record Webrtc - * @param data The data for the request. - * @param data.transcriptId - * @param data.requestBody - * @returns unknown Successful Response - * @throws ApiError - */ - public v1TranscriptRecordWebrtc( - data: V1TranscriptRecordWebrtcData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "POST", - url: "/v1/transcripts/{transcript_id}/record/webrtc", - path: { - transcript_id: data.transcriptId, - }, - body: data.requestBody, - mediaType: "application/json", - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Transcript Process - * @param data The data for the request. - * @param data.transcriptId - * @returns unknown Successful Response - * @throws ApiError - */ - public v1TranscriptProcess( - data: V1TranscriptProcessData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "POST", - url: "/v1/transcripts/{transcript_id}/process", - path: { - transcript_id: data.transcriptId, - }, - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * User Me - * @returns unknown Successful Response - * @throws ApiError - */ - public v1UserMe(): CancelablePromise { - return this.httpRequest.request({ - method: "GET", - url: "/v1/me", - }); - } - - /** - * Zulip Get Streams - * Get all Zulip streams. - * @returns Stream Successful Response - * @throws ApiError - */ - public v1ZulipGetStreams(): CancelablePromise { - return this.httpRequest.request({ - method: "GET", - url: "/v1/zulip/streams", - }); - } - - /** - * Zulip Get Topics - * Get all topics for a specific Zulip stream. - * @param data The data for the request. - * @param data.streamId - * @returns Topic Successful Response - * @throws ApiError - */ - public v1ZulipGetTopics( - data: V1ZulipGetTopicsData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "GET", - url: "/v1/zulip/streams/{stream_id}/topics", - path: { - stream_id: data.streamId, - }, - errors: { - 422: "Validation Error", - }, - }); - } - - /** - * Whereby Webhook - * @param data The data for the request. - * @param data.requestBody - * @returns unknown Successful Response - * @throws ApiError - */ - public v1WherebyWebhook( - data: V1WherebyWebhookData, - ): CancelablePromise { - return this.httpRequest.request({ - method: "POST", - url: "/v1/whereby", - body: data.requestBody, - mediaType: "application/json", - errors: { - 422: "Validation Error", - }, - }); - } -} diff --git a/www/app/api/types.gen.ts b/www/app/api/types.gen.ts index d724fc98..e69de29b 100644 --- a/www/app/api/types.gen.ts +++ b/www/app/api/types.gen.ts @@ -1,1143 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type AudioWaveform = { - data: Array; -}; - -export type Body_transcript_record_upload_v1_transcripts__transcript_id__record_upload_post = - { - chunk: Blob | File; - }; - -export type CreateParticipant = { - speaker?: number | null; - name: string; -}; - -export type CreateRoom = { - name: string; - zulip_auto_post: boolean; - zulip_stream: string; - zulip_topic: string; - is_locked: boolean; - room_mode: string; - recording_type: string; - recording_trigger: string; - is_shared: boolean; - webhook_url: string; - webhook_secret: string; -}; - -export type CreateTranscript = { - name: string; - source_language?: string; - target_language?: string; - source_kind?: SourceKind | null; -}; - -export type DeletionStatus = { - status: string; -}; - -export type GetTranscript = { - id: string; - user_id: string | null; - name: string; - status: string; - locked: boolean; - duration: number; - title: string | null; - short_summary: string | null; - long_summary: string | null; - created_at: string; - share_mode?: string; - source_language: string | null; - target_language: string | null; - reviewed: boolean; - meeting_id: string | null; - source_kind: SourceKind; - room_id?: string | null; - room_name?: string | null; - audio_deleted?: boolean | null; - participants: Array | null; -}; - -export type GetTranscriptMinimal = { - id: string; - user_id: string | null; - name: string; - status: string; - locked: boolean; - duration: number; - title: string | null; - short_summary: string | null; - long_summary: string | null; - created_at: string; - share_mode?: string; - source_language: string | null; - target_language: string | null; - reviewed: boolean; - meeting_id: string | null; - source_kind: SourceKind; - room_id?: string | null; - room_name?: string | null; - audio_deleted?: boolean | null; -}; - -export type GetTranscriptSegmentTopic = { - text: string; - start: number; - speaker: number; -}; - -export type GetTranscriptTopic = { - id: string; - title: string; - summary: string; - timestamp: number; - duration: number | null; - transcript: string; - segments?: Array; -}; - -export type GetTranscriptTopicWithWords = { - id: string; - title: string; - summary: string; - timestamp: number; - duration: number | null; - transcript: string; - segments?: Array; - words?: Array; -}; - -export type GetTranscriptTopicWithWordsPerSpeaker = { - id: string; - title: string; - summary: string; - timestamp: number; - duration: number | null; - transcript: string; - segments?: Array; - words_per_speaker?: Array; -}; - -export type HTTPValidationError = { - detail?: Array; -}; - -export type Meeting = { - id: string; - room_name: string; - room_url: string; - host_room_url: string; - start_date: string; - end_date: string; - recording_type?: "none" | "local" | "cloud"; -}; - -export type recording_type = "none" | "local" | "cloud"; - -export type MeetingConsentRequest = { - consent_given: boolean; -}; - -export type Page_GetTranscriptMinimal_ = { - items: Array; - total?: number | null; - page: number | null; - size: number | null; - pages?: number | null; -}; - -export type Page_RoomDetails_ = { - items: Array; - total?: number | null; - page: number | null; - size: number | null; - pages?: number | null; -}; - -export type Participant = { - id: string; - speaker: number | null; - name: string; -}; - -export type Room = { - id: string; - name: string; - user_id: string; - created_at: string; - zulip_auto_post: boolean; - zulip_stream: string; - zulip_topic: string; - is_locked: boolean; - room_mode: string; - recording_type: string; - recording_trigger: string; - is_shared: boolean; -}; - -export type RoomDetails = { - id: string; - name: string; - user_id: string; - created_at: string; - zulip_auto_post: boolean; - zulip_stream: string; - zulip_topic: string; - is_locked: boolean; - room_mode: string; - recording_type: string; - recording_trigger: string; - is_shared: boolean; - webhook_url: string | null; - webhook_secret: string | null; -}; - -export type RtcOffer = { - sdp: string; - type: string; -}; - -export type SearchResponse = { - results: Array; - /** - * Total number of search results - */ - total: number; - query?: string | null; - /** - * Results per page - */ - limit: number; - /** - * Number of results to skip - */ - offset: number; -}; - -/** - * Public search result model with computed fields. - */ -export type SearchResult = { - id: string; - title?: string | null; - user_id?: string | null; - room_id?: string | null; - room_name?: string | null; - source_kind: SourceKind; - created_at: string; - status: string; - rank: number; - /** - * Duration in seconds - */ - duration: number | null; - /** - * Text snippets around search matches - */ - search_snippets: Array; - /** - * Total number of matches found in the transcript - */ - total_match_count?: number; -}; - -export type SourceKind = "room" | "live" | "file"; - -export type SpeakerAssignment = { - speaker?: number | null; - participant?: string | null; - timestamp_from: number; - timestamp_to: number; -}; - -export type SpeakerAssignmentStatus = { - status: string; -}; - -export type SpeakerMerge = { - speaker_from: number; - speaker_to: number; -}; - -export type SpeakerWords = { - speaker: number; - words: Array; -}; - -export type Stream = { - stream_id: number; - name: string; -}; - -export type Topic = { - name: string; -}; - -export type TranscriptParticipant = { - id?: string; - speaker: number | null; - name: string; -}; - -export type UpdateParticipant = { - speaker?: number | null; - name?: string | null; -}; - -export type UpdateRoom = { - name: string; - zulip_auto_post: boolean; - zulip_stream: string; - zulip_topic: string; - is_locked: boolean; - room_mode: string; - recording_type: string; - recording_trigger: string; - is_shared: boolean; - webhook_url: string; - webhook_secret: string; -}; - -export type UpdateTranscript = { - name?: string | null; - locked?: boolean | null; - title?: string | null; - short_summary?: string | null; - long_summary?: string | null; - share_mode?: "public" | "semi-private" | "private" | null; - participants?: Array | null; - reviewed?: boolean | null; - audio_deleted?: boolean | null; -}; - -export type UserInfo = { - sub: string; - email: string | null; - email_verified: boolean | null; -}; - -export type ValidationError = { - loc: Array; - msg: string; - type: string; -}; - -export type WebhookTestResult = { - success: boolean; - message?: string; - error?: string; - status_code?: number | null; - response_preview?: string | null; -}; - -export type WherebyWebhookEvent = { - apiVersion: string; - id: string; - createdAt: string; - type: string; - data: { - [key: string]: unknown; - }; -}; - -export type Word = { - text: string; - /** - * Time in seconds with float part - */ - start: number; - /** - * Time in seconds with float part - */ - end: number; - speaker?: number; -}; - -export type MetricsResponse = unknown; - -export type V1MeetingAudioConsentData = { - meetingId: string; - requestBody: MeetingConsentRequest; -}; - -export type V1MeetingAudioConsentResponse = unknown; - -export type V1RoomsListData = { - /** - * Page number - */ - page?: number; - /** - * Page size - */ - size?: number; -}; - -export type V1RoomsListResponse = Page_RoomDetails_; - -export type V1RoomsCreateData = { - requestBody: CreateRoom; -}; - -export type V1RoomsCreateResponse = Room; - -export type V1RoomsGetData = { - roomId: string; -}; - -export type V1RoomsGetResponse = RoomDetails; - -export type V1RoomsUpdateData = { - requestBody: UpdateRoom; - roomId: string; -}; - -export type V1RoomsUpdateResponse = RoomDetails; - -export type V1RoomsDeleteData = { - roomId: string; -}; - -export type V1RoomsDeleteResponse = DeletionStatus; - -export type V1RoomsCreateMeetingData = { - roomName: string; -}; - -export type V1RoomsCreateMeetingResponse = Meeting; - -export type V1RoomsTestWebhookData = { - roomId: string; -}; - -export type V1RoomsTestWebhookResponse = WebhookTestResult; - -export type V1TranscriptsListData = { - /** - * Page number - */ - page?: number; - roomId?: string | null; - searchTerm?: string | null; - /** - * Page size - */ - size?: number; - sourceKind?: SourceKind | null; -}; - -export type V1TranscriptsListResponse = Page_GetTranscriptMinimal_; - -export type V1TranscriptsCreateData = { - requestBody: CreateTranscript; -}; - -export type V1TranscriptsCreateResponse = GetTranscript; - -export type V1TranscriptsSearchData = { - /** - * Results per page - */ - limit?: number; - /** - * Number of results to skip - */ - offset?: number; - /** - * Search query text - */ - q: string; - roomId?: string | null; - sourceKind?: SourceKind | null; -}; - -export type V1TranscriptsSearchResponse = SearchResponse; - -export type V1TranscriptGetData = { - transcriptId: string; -}; - -export type V1TranscriptGetResponse = GetTranscript; - -export type V1TranscriptUpdateData = { - requestBody: UpdateTranscript; - transcriptId: string; -}; - -export type V1TranscriptUpdateResponse = GetTranscript; - -export type V1TranscriptDeleteData = { - transcriptId: string; -}; - -export type V1TranscriptDeleteResponse = DeletionStatus; - -export type V1TranscriptGetTopicsData = { - transcriptId: string; -}; - -export type V1TranscriptGetTopicsResponse = Array; - -export type V1TranscriptGetTopicsWithWordsData = { - transcriptId: string; -}; - -export type V1TranscriptGetTopicsWithWordsResponse = - Array; - -export type V1TranscriptGetTopicsWithWordsPerSpeakerData = { - topicId: string; - transcriptId: string; -}; - -export type V1TranscriptGetTopicsWithWordsPerSpeakerResponse = - GetTranscriptTopicWithWordsPerSpeaker; - -export type V1TranscriptPostToZulipData = { - includeTopics: boolean; - stream: string; - topic: string; - transcriptId: string; -}; - -export type V1TranscriptPostToZulipResponse = unknown; - -export type V1TranscriptHeadAudioMp3Data = { - token?: string | null; - transcriptId: string; -}; - -export type V1TranscriptHeadAudioMp3Response = unknown; - -export type V1TranscriptGetAudioMp3Data = { - token?: string | null; - transcriptId: string; -}; - -export type V1TranscriptGetAudioMp3Response = unknown; - -export type V1TranscriptGetAudioWaveformData = { - transcriptId: string; -}; - -export type V1TranscriptGetAudioWaveformResponse = AudioWaveform; - -export type V1TranscriptGetParticipantsData = { - transcriptId: string; -}; - -export type V1TranscriptGetParticipantsResponse = Array; - -export type V1TranscriptAddParticipantData = { - requestBody: CreateParticipant; - transcriptId: string; -}; - -export type V1TranscriptAddParticipantResponse = Participant; - -export type V1TranscriptGetParticipantData = { - participantId: string; - transcriptId: string; -}; - -export type V1TranscriptGetParticipantResponse = Participant; - -export type V1TranscriptUpdateParticipantData = { - participantId: string; - requestBody: UpdateParticipant; - transcriptId: string; -}; - -export type V1TranscriptUpdateParticipantResponse = Participant; - -export type V1TranscriptDeleteParticipantData = { - participantId: string; - transcriptId: string; -}; - -export type V1TranscriptDeleteParticipantResponse = DeletionStatus; - -export type V1TranscriptAssignSpeakerData = { - requestBody: SpeakerAssignment; - transcriptId: string; -}; - -export type V1TranscriptAssignSpeakerResponse = SpeakerAssignmentStatus; - -export type V1TranscriptMergeSpeakerData = { - requestBody: SpeakerMerge; - transcriptId: string; -}; - -export type V1TranscriptMergeSpeakerResponse = SpeakerAssignmentStatus; - -export type V1TranscriptRecordUploadData = { - chunkNumber: number; - formData: Body_transcript_record_upload_v1_transcripts__transcript_id__record_upload_post; - totalChunks: number; - transcriptId: string; -}; - -export type V1TranscriptRecordUploadResponse = unknown; - -export type V1TranscriptGetWebsocketEventsData = { - transcriptId: string; -}; - -export type V1TranscriptGetWebsocketEventsResponse = unknown; - -export type V1TranscriptRecordWebrtcData = { - requestBody: RtcOffer; - transcriptId: string; -}; - -export type V1TranscriptRecordWebrtcResponse = unknown; - -export type V1TranscriptProcessData = { - transcriptId: string; -}; - -export type V1TranscriptProcessResponse = unknown; - -export type V1UserMeResponse = UserInfo | null; - -export type V1ZulipGetStreamsResponse = Array; - -export type V1ZulipGetTopicsData = { - streamId: number; -}; - -export type V1ZulipGetTopicsResponse = Array; - -export type V1WherebyWebhookData = { - requestBody: WherebyWebhookEvent; -}; - -export type V1WherebyWebhookResponse = unknown; - -export type $OpenApiTs = { - "/metrics": { - get: { - res: { - /** - * Successful Response - */ - 200: unknown; - }; - }; - }; - "/v1/meetings/{meeting_id}/consent": { - post: { - req: V1MeetingAudioConsentData; - res: { - /** - * Successful Response - */ - 200: unknown; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - "/v1/rooms": { - get: { - req: V1RoomsListData; - res: { - /** - * Successful Response - */ - 200: Page_RoomDetails_; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - post: { - req: V1RoomsCreateData; - res: { - /** - * Successful Response - */ - 200: Room; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - "/v1/rooms/{room_id}": { - get: { - req: V1RoomsGetData; - res: { - /** - * Successful Response - */ - 200: RoomDetails; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - patch: { - req: V1RoomsUpdateData; - res: { - /** - * Successful Response - */ - 200: RoomDetails; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - delete: { - req: V1RoomsDeleteData; - res: { - /** - * Successful Response - */ - 200: DeletionStatus; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - "/v1/rooms/{room_name}/meeting": { - post: { - req: V1RoomsCreateMeetingData; - res: { - /** - * Successful Response - */ - 200: Meeting; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - "/v1/rooms/{room_id}/webhook/test": { - post: { - req: V1RoomsTestWebhookData; - res: { - /** - * Successful Response - */ - 200: WebhookTestResult; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - "/v1/transcripts": { - get: { - req: V1TranscriptsListData; - res: { - /** - * Successful Response - */ - 200: Page_GetTranscriptMinimal_; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - post: { - req: V1TranscriptsCreateData; - res: { - /** - * Successful Response - */ - 200: GetTranscript; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - "/v1/transcripts/search": { - get: { - req: V1TranscriptsSearchData; - res: { - /** - * Successful Response - */ - 200: SearchResponse; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - "/v1/transcripts/{transcript_id}": { - get: { - req: V1TranscriptGetData; - res: { - /** - * Successful Response - */ - 200: GetTranscript; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - patch: { - req: V1TranscriptUpdateData; - res: { - /** - * Successful Response - */ - 200: GetTranscript; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - delete: { - req: V1TranscriptDeleteData; - res: { - /** - * Successful Response - */ - 200: DeletionStatus; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - "/v1/transcripts/{transcript_id}/topics": { - get: { - req: V1TranscriptGetTopicsData; - res: { - /** - * Successful Response - */ - 200: Array; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - "/v1/transcripts/{transcript_id}/topics/with-words": { - get: { - req: V1TranscriptGetTopicsWithWordsData; - res: { - /** - * Successful Response - */ - 200: Array; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - "/v1/transcripts/{transcript_id}/topics/{topic_id}/words-per-speaker": { - get: { - req: V1TranscriptGetTopicsWithWordsPerSpeakerData; - res: { - /** - * Successful Response - */ - 200: GetTranscriptTopicWithWordsPerSpeaker; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - "/v1/transcripts/{transcript_id}/zulip": { - post: { - req: V1TranscriptPostToZulipData; - res: { - /** - * Successful Response - */ - 200: unknown; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - "/v1/transcripts/{transcript_id}/audio/mp3": { - head: { - req: V1TranscriptHeadAudioMp3Data; - res: { - /** - * Successful Response - */ - 200: unknown; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - get: { - req: V1TranscriptGetAudioMp3Data; - res: { - /** - * Successful Response - */ - 200: unknown; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - "/v1/transcripts/{transcript_id}/audio/waveform": { - get: { - req: V1TranscriptGetAudioWaveformData; - res: { - /** - * Successful Response - */ - 200: AudioWaveform; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - "/v1/transcripts/{transcript_id}/participants": { - get: { - req: V1TranscriptGetParticipantsData; - res: { - /** - * Successful Response - */ - 200: Array; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - post: { - req: V1TranscriptAddParticipantData; - res: { - /** - * Successful Response - */ - 200: Participant; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - "/v1/transcripts/{transcript_id}/participants/{participant_id}": { - get: { - req: V1TranscriptGetParticipantData; - res: { - /** - * Successful Response - */ - 200: Participant; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - patch: { - req: V1TranscriptUpdateParticipantData; - res: { - /** - * Successful Response - */ - 200: Participant; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - delete: { - req: V1TranscriptDeleteParticipantData; - res: { - /** - * Successful Response - */ - 200: DeletionStatus; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - "/v1/transcripts/{transcript_id}/speaker/assign": { - patch: { - req: V1TranscriptAssignSpeakerData; - res: { - /** - * Successful Response - */ - 200: SpeakerAssignmentStatus; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - "/v1/transcripts/{transcript_id}/speaker/merge": { - patch: { - req: V1TranscriptMergeSpeakerData; - res: { - /** - * Successful Response - */ - 200: SpeakerAssignmentStatus; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - "/v1/transcripts/{transcript_id}/record/upload": { - post: { - req: V1TranscriptRecordUploadData; - res: { - /** - * Successful Response - */ - 200: unknown; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - "/v1/transcripts/{transcript_id}/events": { - get: { - req: V1TranscriptGetWebsocketEventsData; - res: { - /** - * Successful Response - */ - 200: unknown; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - "/v1/transcripts/{transcript_id}/record/webrtc": { - post: { - req: V1TranscriptRecordWebrtcData; - res: { - /** - * Successful Response - */ - 200: unknown; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - "/v1/transcripts/{transcript_id}/process": { - post: { - req: V1TranscriptProcessData; - res: { - /** - * Successful Response - */ - 200: unknown; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - "/v1/me": { - get: { - res: { - /** - * Successful Response - */ - 200: UserInfo | null; - }; - }; - }; - "/v1/zulip/streams": { - get: { - res: { - /** - * Successful Response - */ - 200: Array; - }; - }; - }; - "/v1/zulip/streams/{stream_id}/topics": { - get: { - req: V1ZulipGetTopicsData; - res: { - /** - * Successful Response - */ - 200: Array; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; - "/v1/whereby": { - post: { - req: V1WherebyWebhookData; - res: { - /** - * Successful Response - */ - 200: unknown; - /** - * Validation Error - */ - 422: HTTPValidationError; - }; - }; - }; -}; diff --git a/www/app/api/urls.ts b/www/app/api/urls.ts index bd0a910c..89ce5af8 100644 --- a/www/app/api/urls.ts +++ b/www/app/api/urls.ts @@ -1,2 +1 @@ -// TODO better connection with generated schema; it's duplication export const RECORD_A_MEETING_URL = "/transcripts/new" as const; diff --git a/www/app/layout.tsx b/www/app/layout.tsx index f73b8813..62175be9 100644 --- a/www/app/layout.tsx +++ b/www/app/layout.tsx @@ -1,7 +1,6 @@ import "./styles/globals.scss"; import { Metadata, Viewport } from "next"; import { Poppins } from "next/font/google"; -import SessionProvider from "./lib/SessionProvider"; import { ErrorProvider } from "./(errors)/errorContext"; import ErrorMessage from "./(errors)/errorMessage"; import { DomainContextProvider } from "./domainContext"; @@ -74,18 +73,16 @@ export default async function RootLayout({ return ( - - - - "something went really wrong"

}> - - - {children} - -
-
-
-
+ + + "something went really wrong"

}> + + + {children} + +
+
+
); diff --git a/www/app/lib/AuthProvider.tsx b/www/app/lib/AuthProvider.tsx new file mode 100644 index 00000000..96f49f87 --- /dev/null +++ b/www/app/lib/AuthProvider.tsx @@ -0,0 +1,104 @@ +"use client"; + +import { createContext, useContext } from "react"; +import { useSession as useNextAuthSession } from "next-auth/react"; +import { signOut, signIn } from "next-auth/react"; +import { configureApiAuth } from "./apiClient"; +import { assertCustomSession, CustomSession } from "./types"; +import { Session } from "next-auth"; +import { SessionAutoRefresh } from "./SessionAutoRefresh"; +import { REFRESH_ACCESS_TOKEN_ERROR } from "./auth"; + +type AuthContextType = ( + | { status: "loading" } + | { status: "refreshing" } + | { status: "unauthenticated"; error?: string } + | { + status: "authenticated"; + accessToken: string; + accessTokenExpires: number; + user: CustomSession["user"]; + } +) & { + update: () => Promise; + signIn: typeof signIn; + signOut: typeof signOut; +}; + +const AuthContext = createContext(undefined); + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const { data: session, status, update } = useNextAuthSession(); + const customSession = session ? assertCustomSession(session) : null; + + const contextValue: AuthContextType = { + ...(() => { + switch (status) { + case "loading": { + const sessionIsHere = !!customSession; + switch (sessionIsHere) { + case false: { + return { status }; + } + case true: { + return { status: "refreshing" as const }; + } + default: { + const _: never = sessionIsHere; + throw new Error("unreachable"); + } + } + } + case "authenticated": { + if (customSession?.error === REFRESH_ACCESS_TOKEN_ERROR) { + // token had expired but next auth still returns "authenticated" so show user unauthenticated state + return { + status: "unauthenticated" as const, + }; + } else if (customSession?.accessToken) { + return { + status, + accessToken: customSession.accessToken, + accessTokenExpires: customSession.accessTokenExpires, + user: customSession.user, + }; + } else { + console.warn( + "illegal state: authenticated but have no session/or access token. ignoring", + ); + return { status: "unauthenticated" as const }; + } + } + case "unauthenticated": { + return { status: "unauthenticated" as const }; + } + default: { + const _: never = status; + throw new Error("unreachable"); + } + } + })(), + update, + signIn, + signOut, + }; + + // not useEffect, we need it ASAP + configureApiAuth( + contextValue.status === "authenticated" ? contextValue.accessToken : null, + ); + + return ( + + {children} + + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +} diff --git a/www/app/lib/SessionAutoRefresh.tsx b/www/app/lib/SessionAutoRefresh.tsx index 1e230d6c..fd29367f 100644 --- a/www/app/lib/SessionAutoRefresh.tsx +++ b/www/app/lib/SessionAutoRefresh.tsx @@ -1,5 +1,5 @@ /** - * This is a custom hook that automatically refreshes the session when the access token is about to expire. + * This is a custom provider that automatically refreshes the session when the access token is about to expire. * When communicating with the reflector API, we need to ensure that the access token is always valid. * * We could have implemented that as an interceptor on the API client, but not everything is using the @@ -7,30 +7,38 @@ */ "use client"; -import { useSession } from "next-auth/react"; import { useEffect } from "react"; -import { CustomSession } from "./types"; +import { useAuth } from "./AuthProvider"; +import { REFRESH_ACCESS_TOKEN_BEFORE } from "./auth"; -export function SessionAutoRefresh({ - children, - refreshInterval = 20 /* seconds */, -}) { - const { data: session, update } = useSession(); - const customSession = session as CustomSession; - const accessTokenExpires = customSession?.accessTokenExpires; +const REFRESH_BEFORE = REFRESH_ACCESS_TOKEN_BEFORE; + +export function SessionAutoRefresh({ children }) { + const auth = useAuth(); + const accessTokenExpires = + auth.status === "authenticated" ? auth.accessTokenExpires : null; useEffect(() => { + // technical value for how often the setInterval will be polling news - not too fast (no spam in case of errors) + // and not too slow (debuggable) + const INTERVAL_REFRESH_MS = 5000; const interval = setInterval(() => { - if (accessTokenExpires) { + if (accessTokenExpires !== null) { const timeLeft = accessTokenExpires - Date.now(); - if (timeLeft < refreshInterval * 1000) { - update(); + if (timeLeft < REFRESH_BEFORE) { + auth + .update() + .then(() => {}) + .catch((e) => { + // note: 401 won't be considered error here + console.error("error refreshing auth token", e); + }); } } - }, refreshInterval * 1000); + }, INTERVAL_REFRESH_MS); return () => clearInterval(interval); - }, [accessTokenExpires, refreshInterval, update]); + }, [accessTokenExpires, auth.update]); return children; } diff --git a/www/app/lib/SessionProvider.tsx b/www/app/lib/SessionProvider.tsx deleted file mode 100644 index 9c95fbc8..00000000 --- a/www/app/lib/SessionProvider.tsx +++ /dev/null @@ -1,11 +0,0 @@ -"use client"; -import { SessionProvider as SessionProviderNextAuth } from "next-auth/react"; -import { SessionAutoRefresh } from "./SessionAutoRefresh"; - -export default function SessionProvider({ children }) { - return ( - - {children} - - ); -} diff --git a/www/app/lib/__tests__/redisTokenCache.test.ts b/www/app/lib/__tests__/redisTokenCache.test.ts new file mode 100644 index 00000000..8ca8e8a1 --- /dev/null +++ b/www/app/lib/__tests__/redisTokenCache.test.ts @@ -0,0 +1,85 @@ +import { + getTokenCache, + setTokenCache, + deleteTokenCache, + TokenCacheEntry, + KV, +} from "../redisTokenCache"; + +const mockKV: KV & { + clear: () => void; +} = (() => { + const data = new Map(); + return { + async get(key: string): Promise { + return data.get(key) || null; + }, + + async setex(key: string, seconds_: number, value: string): Promise<"OK"> { + data.set(key, value); + return "OK"; + }, + + async del(key: string): Promise { + const existed = data.has(key); + data.delete(key); + return existed ? 1 : 0; + }, + + clear() { + data.clear(); + }, + }; +})(); + +describe("Redis Token Cache", () => { + beforeEach(() => { + mockKV.clear(); + }); + + test("basic write/read - value written equals value read", async () => { + const testKey = "token:test-user-123"; + const testValue: TokenCacheEntry = { + token: { + sub: "test-user-123", + name: "Test User", + email: "test@example.com", + accessToken: "access-token-123", + accessTokenExpires: Date.now() + 3600000, // 1 hour from now + refreshToken: "refresh-token-456", + }, + timestamp: Date.now(), + }; + + await setTokenCache(mockKV, testKey, testValue); + const retrievedValue = await getTokenCache(mockKV, testKey); + + expect(retrievedValue).not.toBeNull(); + expect(retrievedValue).toEqual(testValue); + expect(retrievedValue?.token.accessToken).toBe(testValue.token.accessToken); + expect(retrievedValue?.token.sub).toBe(testValue.token.sub); + expect(retrievedValue?.timestamp).toBe(testValue.timestamp); + }); + + test("get returns null for non-existent key", async () => { + const result = await getTokenCache(mockKV, "non-existent-key"); + expect(result).toBeNull(); + }); + + test("delete removes token from cache", async () => { + const testKey = "token:delete-test"; + const testValue: TokenCacheEntry = { + token: { + accessToken: "test-token", + accessTokenExpires: Date.now() + 3600000, + }, + timestamp: Date.now(), + }; + + await setTokenCache(mockKV, testKey, testValue); + await deleteTokenCache(mockKV, testKey); + + const result = await getTokenCache(mockKV, testKey); + expect(result).toBeNull(); + }); +}); diff --git a/www/app/lib/apiClient.tsx b/www/app/lib/apiClient.tsx new file mode 100644 index 00000000..cd97e151 --- /dev/null +++ b/www/app/lib/apiClient.tsx @@ -0,0 +1,50 @@ +"use client"; + +import createClient from "openapi-fetch"; +import type { paths } from "../reflector-api"; +import { + queryOptions, + useMutation, + useQuery, + useSuspenseQuery, +} from "@tanstack/react-query"; +import createFetchClient from "openapi-react-query"; +import { assertExistsAndNonEmptyString } from "./utils"; +import { isBuildPhase } from "./next"; + +const API_URL = !isBuildPhase + ? assertExistsAndNonEmptyString(process.env.NEXT_PUBLIC_API_URL) + : "http://localhost"; + +// Create the base openapi-fetch client with a default URL +// The actual URL will be set via middleware in AuthProvider +export const client = createClient({ + baseUrl: API_URL, +}); + +export const $api = createFetchClient(client); + +let currentAuthToken: string | null | undefined = null; + +client.use({ + onRequest({ request }) { + if (currentAuthToken) { + request.headers.set("Authorization", `Bearer ${currentAuthToken}`); + } + // XXX Only set Content-Type if not already set (FormData will set its own boundary) + // This is a work around for uploading file, we're passing a formdata + // but the content type was still application/json + if ( + !request.headers.has("Content-Type") && + !(request.body instanceof FormData) + ) { + request.headers.set("Content-Type", "application/json"); + } + return request; + }, +}); + +// the function contract: lightweight, idempotent +export const configureApiAuth = (token: string | null | undefined) => { + currentAuthToken = token; +}; diff --git a/www/app/lib/apiHooks.ts b/www/app/lib/apiHooks.ts new file mode 100644 index 00000000..94d84c9b --- /dev/null +++ b/www/app/lib/apiHooks.ts @@ -0,0 +1,618 @@ +"use client"; + +import { $api } from "./apiClient"; +import { useError } from "../(errors)/errorContext"; +import { useQueryClient } from "@tanstack/react-query"; +import type { components } from "../reflector-api"; +import { useAuth } from "./AuthProvider"; + +/* + * XXX error types returned from the hooks are not always correct; declared types are ValidationError but real type could be string or any other + * this is either a limitation or incorrect usage of Python json schema generator + * or, limitation or incorrect usage of .d type generator from json schema + * */ + +const useAuthReady = () => { + const auth = useAuth(); + + return { + isAuthenticated: auth.status === "authenticated", + isLoading: auth.status === "loading", + }; +}; + +export function useRoomsList(page: number = 1) { + const { isAuthenticated } = useAuthReady(); + + return $api.useQuery( + "get", + "/v1/rooms", + { + params: { + query: { page }, + }, + }, + { + enabled: isAuthenticated, + }, + ); +} + +type SourceKind = components["schemas"]["SourceKind"]; + +export function useTranscriptsSearch( + q: string = "", + options: { + limit?: number; + offset?: number; + room_id?: string; + source_kind?: SourceKind; + } = {}, +) { + return $api.useQuery( + "get", + "/v1/transcripts/search", + { + params: { + query: { + q, + limit: options.limit, + offset: options.offset, + room_id: options.room_id, + source_kind: options.source_kind, + }, + }, + }, + { + enabled: true, + }, + ); +} + +export function useTranscriptDelete() { + const { setError } = useError(); + const queryClient = useQueryClient(); + + return $api.useMutation("delete", "/v1/transcripts/{transcript_id}", { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ["get", "/v1/transcripts/search"], + }); + }, + onError: (error) => { + setError(error as Error, "There was an error deleting the transcript"); + }, + }); +} + +export function useTranscriptProcess() { + const { setError } = useError(); + + return $api.useMutation("post", "/v1/transcripts/{transcript_id}/process", { + onError: (error) => { + setError(error as Error, "There was an error processing the transcript"); + }, + }); +} + +export function useTranscriptGet(transcriptId: string | null) { + const { isAuthenticated } = useAuthReady(); + + return $api.useQuery( + "get", + "/v1/transcripts/{transcript_id}", + { + params: { + path: { + transcript_id: transcriptId || "", + }, + }, + }, + { + enabled: !!transcriptId && isAuthenticated, + }, + ); +} + +export function useRoomGet(roomId: string | null) { + const { isAuthenticated } = useAuthReady(); + + return $api.useQuery( + "get", + "/v1/rooms/{room_id}", + { + params: { + path: { room_id: roomId || "" }, + }, + }, + { + enabled: !!roomId && isAuthenticated, + }, + ); +} + +export function useRoomTestWebhook() { + const { setError } = useError(); + + return $api.useMutation("post", "/v1/rooms/{room_id}/webhook/test", { + onError: (error) => { + setError(error as Error, "There was an error testing the webhook"); + }, + }); +} + +export function useRoomCreate() { + const { setError } = useError(); + const queryClient = useQueryClient(); + + return $api.useMutation("post", "/v1/rooms", { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: $api.queryOptions("get", "/v1/rooms").queryKey, + }); + }, + onError: (error) => { + setError(error as Error, "There was an error creating the room"); + }, + }); +} + +export function useRoomUpdate() { + const { setError } = useError(); + const queryClient = useQueryClient(); + + return $api.useMutation("patch", "/v1/rooms/{room_id}", { + onSuccess: async (room) => { + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: $api.queryOptions("get", "/v1/rooms").queryKey, + }), + queryClient.invalidateQueries({ + queryKey: $api.queryOptions("get", "/v1/rooms/{room_id}", { + params: { + path: { + room_id: room.id, + }, + }, + }).queryKey, + }), + ]); + }, + onError: (error) => { + setError(error as Error, "There was an error updating the room"); + }, + }); +} + +export function useRoomDelete() { + const { setError } = useError(); + const queryClient = useQueryClient(); + + return $api.useMutation("delete", "/v1/rooms/{room_id}", { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: $api.queryOptions("get", "/v1/rooms").queryKey, + }); + }, + onError: (error) => { + setError(error as Error, "There was an error deleting the room"); + }, + }); +} + +export function useZulipStreams() { + const { isAuthenticated } = useAuthReady(); + + return $api.useQuery( + "get", + "/v1/zulip/streams", + {}, + { + enabled: isAuthenticated, + }, + ); +} + +export function useZulipTopics(streamId: number | null) { + const { isAuthenticated } = useAuthReady(); + const enabled = !!streamId && isAuthenticated; + return $api.useQuery( + "get", + "/v1/zulip/streams/{stream_id}/topics", + { + params: { + path: { + stream_id: enabled ? streamId : 0, + }, + }, + }, + { + enabled, + }, + ); +} + +export function useTranscriptUpdate() { + const { setError } = useError(); + const queryClient = useQueryClient(); + + return $api.useMutation("patch", "/v1/transcripts/{transcript_id}", { + onSuccess: (data, variables) => { + queryClient.invalidateQueries({ + queryKey: $api.queryOptions("get", "/v1/transcripts/{transcript_id}", { + params: { + path: { transcript_id: variables.params.path.transcript_id }, + }, + }).queryKey, + }); + }, + onError: (error) => { + setError(error as Error, "There was an error updating the transcript"); + }, + }); +} + +export function useTranscriptPostToZulip() { + const { setError } = useError(); + + // @ts-ignore - Zulip endpoint not in OpenAPI spec + return $api.useMutation("post", "/v1/transcripts/{transcript_id}/zulip", { + onError: (error) => { + setError(error as Error, "There was an error posting to Zulip"); + }, + }); +} + +export function useTranscriptUploadAudio() { + const { setError } = useError(); + const queryClient = useQueryClient(); + + return $api.useMutation( + "post", + "/v1/transcripts/{transcript_id}/record/upload", + { + onSuccess: (data, variables) => { + queryClient.invalidateQueries({ + queryKey: $api.queryOptions( + "get", + "/v1/transcripts/{transcript_id}", + { + params: { + path: { transcript_id: variables.params.path.transcript_id }, + }, + }, + ).queryKey, + }); + }, + onError: (error) => { + setError(error as Error, "There was an error uploading the audio file"); + }, + }, + ); +} + +export function useTranscriptWaveform(transcriptId: string | null) { + const { isAuthenticated } = useAuthReady(); + + return $api.useQuery( + "get", + "/v1/transcripts/{transcript_id}/audio/waveform", + { + params: { + path: { transcript_id: transcriptId || "" }, + }, + }, + { + enabled: !!transcriptId && isAuthenticated, + }, + ); +} + +export function useTranscriptMP3(transcriptId: string | null) { + const { isAuthenticated } = useAuthReady(); + + return $api.useQuery( + "get", + "/v1/transcripts/{transcript_id}/audio/mp3", + { + params: { + path: { transcript_id: transcriptId || "" }, + }, + }, + { + enabled: !!transcriptId && isAuthenticated, + }, + ); +} + +export function useTranscriptTopics(transcriptId: string | null) { + const { isAuthenticated } = useAuthReady(); + + return $api.useQuery( + "get", + "/v1/transcripts/{transcript_id}/topics", + { + params: { + path: { transcript_id: transcriptId || "" }, + }, + }, + { + enabled: !!transcriptId && isAuthenticated, + }, + ); +} + +export function useTranscriptTopicsWithWords(transcriptId: string | null) { + const { isAuthenticated } = useAuthReady(); + + return $api.useQuery( + "get", + "/v1/transcripts/{transcript_id}/topics/with-words", + { + params: { + path: { transcript_id: transcriptId || "" }, + }, + }, + { + enabled: !!transcriptId && isAuthenticated, + }, + ); +} + +export function useTranscriptTopicsWithWordsPerSpeaker( + transcriptId: string | null, + topicId: string | null, +) { + const { isAuthenticated } = useAuthReady(); + + return $api.useQuery( + "get", + "/v1/transcripts/{transcript_id}/topics/{topic_id}/words-per-speaker", + { + params: { + path: { + transcript_id: transcriptId || "", + topic_id: topicId || "", + }, + }, + }, + { + enabled: !!transcriptId && !!topicId && isAuthenticated, + }, + ); +} + +export function useTranscriptParticipants(transcriptId: string | null) { + const { isAuthenticated } = useAuthReady(); + + return $api.useQuery( + "get", + "/v1/transcripts/{transcript_id}/participants", + { + params: { + path: { transcript_id: transcriptId || "" }, + }, + }, + { + enabled: !!transcriptId && isAuthenticated, + }, + ); +} + +export function useTranscriptParticipantUpdate() { + const { setError } = useError(); + const queryClient = useQueryClient(); + + return $api.useMutation( + "patch", + "/v1/transcripts/{transcript_id}/participants/{participant_id}", + { + onSuccess: (data, variables) => { + queryClient.invalidateQueries({ + queryKey: $api.queryOptions( + "get", + "/v1/transcripts/{transcript_id}/participants", + { + params: { + path: { transcript_id: variables.params.path.transcript_id }, + }, + }, + ).queryKey, + }); + }, + onError: (error) => { + setError(error as Error, "There was an error updating the participant"); + }, + }, + ); +} + +export function useTranscriptParticipantCreate() { + const { setError } = useError(); + const queryClient = useQueryClient(); + + return $api.useMutation( + "post", + "/v1/transcripts/{transcript_id}/participants", + { + onSuccess: (data, variables) => { + queryClient.invalidateQueries({ + queryKey: $api.queryOptions( + "get", + "/v1/transcripts/{transcript_id}/participants", + { + params: { + path: { transcript_id: variables.params.path.transcript_id }, + }, + }, + ).queryKey, + }); + }, + onError: (error) => { + setError(error as Error, "There was an error creating the participant"); + }, + }, + ); +} + +export function useTranscriptParticipantDelete() { + const { setError } = useError(); + const queryClient = useQueryClient(); + + return $api.useMutation( + "delete", + "/v1/transcripts/{transcript_id}/participants/{participant_id}", + { + onSuccess: (data, variables) => { + queryClient.invalidateQueries({ + queryKey: $api.queryOptions( + "get", + "/v1/transcripts/{transcript_id}/participants", + { + params: { + path: { transcript_id: variables.params.path.transcript_id }, + }, + }, + ).queryKey, + }); + }, + onError: (error) => { + setError(error as Error, "There was an error deleting the participant"); + }, + }, + ); +} + +export function useTranscriptSpeakerAssign() { + const { setError } = useError(); + const queryClient = useQueryClient(); + + return $api.useMutation( + "patch", + "/v1/transcripts/{transcript_id}/speaker/assign", + { + onSuccess: (data, variables) => { + queryClient.invalidateQueries({ + queryKey: $api.queryOptions( + "get", + "/v1/transcripts/{transcript_id}", + { + params: { + path: { transcript_id: variables.params.path.transcript_id }, + }, + }, + ).queryKey, + }); + queryClient.invalidateQueries({ + queryKey: $api.queryOptions( + "get", + "/v1/transcripts/{transcript_id}/participants", + { + params: { + path: { transcript_id: variables.params.path.transcript_id }, + }, + }, + ).queryKey, + }); + }, + onError: (error) => { + setError(error as Error, "There was an error assigning the speaker"); + }, + }, + ); +} + +export function useTranscriptSpeakerMerge() { + const { setError } = useError(); + const queryClient = useQueryClient(); + + return $api.useMutation( + "patch", + "/v1/transcripts/{transcript_id}/speaker/merge", + { + onSuccess: (data, variables) => { + queryClient.invalidateQueries({ + queryKey: $api.queryOptions( + "get", + "/v1/transcripts/{transcript_id}", + { + params: { + path: { transcript_id: variables.params.path.transcript_id }, + }, + }, + ).queryKey, + }); + queryClient.invalidateQueries({ + queryKey: $api.queryOptions( + "get", + "/v1/transcripts/{transcript_id}/participants", + { + params: { + path: { transcript_id: variables.params.path.transcript_id }, + }, + }, + ).queryKey, + }); + }, + onError: (error) => { + setError(error as Error, "There was an error merging speakers"); + }, + }, + ); +} + +export function useMeetingAudioConsent() { + const { setError } = useError(); + + return $api.useMutation("post", "/v1/meetings/{meeting_id}/consent", { + onError: (error) => { + setError(error as Error, "There was an error recording consent"); + }, + }); +} + +export function useTranscriptWebRTC() { + const { setError } = useError(); + + return $api.useMutation( + "post", + "/v1/transcripts/{transcript_id}/record/webrtc", + { + onError: (error) => { + setError(error as Error, "There was an error with WebRTC connection"); + }, + }, + ); +} + +export function useTranscriptCreate() { + const { setError } = useError(); + const queryClient = useQueryClient(); + + return $api.useMutation("post", "/v1/transcripts", { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ["get", "/v1/transcripts/search"], + }); + }, + onError: (error) => { + setError(error as Error, "There was an error creating the transcript"); + }, + }); +} + +export function useRoomsCreateMeeting() { + const { setError } = useError(); + const queryClient = useQueryClient(); + + return $api.useMutation("post", "/v1/rooms/{room_name}/meeting", { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: $api.queryOptions("get", "/v1/rooms").queryKey, + }); + }, + onError: (error) => { + setError(error as Error, "There was an error creating the meeting"); + }, + }); +} diff --git a/www/app/lib/auth.ts b/www/app/lib/auth.ts index 9169c694..f6e60513 100644 --- a/www/app/lib/auth.ts +++ b/www/app/lib/auth.ts @@ -1,157 +1,13 @@ -// import { kv } from "@vercel/kv"; -import Redlock, { ResourceLockedError } from "redlock"; -import { AuthOptions } from "next-auth"; -import AuthentikProvider from "next-auth/providers/authentik"; -import { JWT } from "next-auth/jwt"; -import { JWTWithAccessToken, CustomSession } from "./types"; -import Redis from "ioredis"; +export const REFRESH_ACCESS_TOKEN_ERROR = "RefreshAccessTokenError" as const; +// 4 min is 1 min less than default authentic value. here we assume that authentic won't be set to access tokens < 4 min +export const REFRESH_ACCESS_TOKEN_BEFORE = 4 * 60 * 1000; -const PRETIMEOUT = 60; // seconds before token expires to refresh it -const DEFAULT_REDIS_KEY_TIMEOUT = 60 * 60 * 24 * 30; // 30 days (refresh token expires in 30 days) -const kv = new Redis(process.env.KV_URL || "", { - tls: {}, -}); -const redlock = new Redlock([kv], {}); +export const LOGIN_REQUIRED_PAGES = [ + "/transcripts/[!new]", + "/browse(.*)", + "/rooms(.*)", +]; -redlock.on("error", (error) => { - if (error instanceof ResourceLockedError) { - return; - } - - // Log all other errors. - console.error(error); -}); - -export const authOptions: AuthOptions = { - providers: [ - AuthentikProvider({ - clientId: process.env.AUTHENTIK_CLIENT_ID as string, - clientSecret: process.env.AUTHENTIK_CLIENT_SECRET as string, - issuer: process.env.AUTHENTIK_ISSUER, - authorization: { - params: { - scope: "openid email profile offline_access", - }, - }, - }), - ], - session: { - strategy: "jwt", - }, - callbacks: { - async jwt({ token, account, user }) { - const extendedToken = token as JWTWithAccessToken; - if (account && user) { - // called only on first login - // XXX account.expires_in used in example is not defined for authentik backend, but expires_at is - const expiresAt = (account.expires_at as number) - PRETIMEOUT; - const jwtToken = { - ...extendedToken, - accessToken: account.access_token, - accessTokenExpires: expiresAt * 1000, - refreshToken: account.refresh_token, - }; - kv.set( - `token:${jwtToken.sub}`, - JSON.stringify(jwtToken), - "EX", - DEFAULT_REDIS_KEY_TIMEOUT, - ); - return jwtToken; - } - - if (Date.now() < extendedToken.accessTokenExpires) { - return token; - } - - // access token has expired, try to update it - return await redisLockedrefreshAccessToken(token); - }, - async session({ session, token }) { - const extendedToken = token as JWTWithAccessToken; - const customSession = session as CustomSession; - customSession.accessToken = extendedToken.accessToken; - customSession.accessTokenExpires = extendedToken.accessTokenExpires; - customSession.error = extendedToken.error; - customSession.user = { - id: extendedToken.sub, - name: extendedToken.name, - email: extendedToken.email, - }; - return customSession; - }, - }, -}; - -async function redisLockedrefreshAccessToken(token: JWT) { - return await redlock.using( - [token.sub as string, "jwt-refresh"], - 5000, - async () => { - const redisToken = await kv.get(`token:${token.sub}`); - const currentToken = JSON.parse( - redisToken as string, - ) as JWTWithAccessToken; - - // if there is multiple requests for the same token, it may already have been refreshed - if (Date.now() < currentToken.accessTokenExpires) { - return currentToken; - } - - // now really do the request - const newToken = await refreshAccessToken(currentToken); - await kv.set( - `token:${currentToken.sub}`, - JSON.stringify(newToken), - "EX", - DEFAULT_REDIS_KEY_TIMEOUT, - ); - return newToken; - }, - ); -} - -async function refreshAccessToken(token: JWT): Promise { - try { - const url = `${process.env.AUTHENTIK_REFRESH_TOKEN_URL}`; - - const options = { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - client_id: process.env.AUTHENTIK_CLIENT_ID as string, - client_secret: process.env.AUTHENTIK_CLIENT_SECRET as string, - grant_type: "refresh_token", - refresh_token: token.refreshToken as string, - }).toString(), - method: "POST", - }; - - const response = await fetch(url, options); - if (!response.ok) { - console.error( - new Date().toISOString(), - "Failed to refresh access token. Response status:", - response.status, - ); - const responseBody = await response.text(); - console.error(new Date().toISOString(), "Response body:", responseBody); - throw new Error(`Failed to refresh access token: ${response.statusText}`); - } - const refreshedTokens = await response.json(); - return { - ...token, - accessToken: refreshedTokens.access_token, - accessTokenExpires: - Date.now() + (refreshedTokens.expires_in - PRETIMEOUT) * 1000, - refreshToken: refreshedTokens.refresh_token, - }; - } catch (error) { - console.error("Error refreshing access token", error); - return { - ...token, - error: "RefreshAccessTokenError", - } as JWTWithAccessToken; - } -} +export const PROTECTED_PAGES = new RegExp( + LOGIN_REQUIRED_PAGES.map((page) => `^${page}$`).join("|"), +); diff --git a/www/app/lib/authBackend.ts b/www/app/lib/authBackend.ts new file mode 100644 index 00000000..af93b274 --- /dev/null +++ b/www/app/lib/authBackend.ts @@ -0,0 +1,178 @@ +import { AuthOptions } from "next-auth"; +import AuthentikProvider from "next-auth/providers/authentik"; +import type { JWT } from "next-auth/jwt"; +import { JWTWithAccessToken, CustomSession } from "./types"; +import { assertExists, assertExistsAndNonEmptyString } from "./utils"; +import { + REFRESH_ACCESS_TOKEN_BEFORE, + REFRESH_ACCESS_TOKEN_ERROR, +} from "./auth"; +import { + getTokenCache, + setTokenCache, + deleteTokenCache, +} from "./redisTokenCache"; +import { tokenCacheRedis } from "./redisClient"; +import { isBuildPhase } from "./next"; + +// REFRESH_ACCESS_TOKEN_BEFORE because refresh is based on access token expiration (imagine we cache it 30 days) +const TOKEN_CACHE_TTL = REFRESH_ACCESS_TOKEN_BEFORE; + +const refreshLocks = new Map>(); + +const CLIENT_ID = !isBuildPhase + ? assertExistsAndNonEmptyString(process.env.AUTHENTIK_CLIENT_ID) + : "noop"; +const CLIENT_SECRET = !isBuildPhase + ? assertExistsAndNonEmptyString(process.env.AUTHENTIK_CLIENT_SECRET) + : "noop"; + +export const authOptions: AuthOptions = { + providers: [ + AuthentikProvider({ + clientId: CLIENT_ID, + clientSecret: CLIENT_SECRET, + issuer: process.env.AUTHENTIK_ISSUER, + authorization: { + params: { + scope: "openid email profile offline_access", + }, + }, + }), + ], + session: { + strategy: "jwt", + }, + callbacks: { + async jwt({ token, account, user }) { + const KEY = `token:${token.sub}`; + + if (account && user) { + // called only on first login + // XXX account.expires_in used in example is not defined for authentik backend, but expires_at is + const expiresAtS = assertExists(account.expires_at); + const expiresAtMs = expiresAtS * 1000; + if (!account.access_token) { + await deleteTokenCache(tokenCacheRedis, KEY); + } else { + const jwtToken: JWTWithAccessToken = { + ...token, + accessToken: account.access_token, + accessTokenExpires: expiresAtMs, + refreshToken: account.refresh_token, + }; + await setTokenCache(tokenCacheRedis, KEY, { + token: jwtToken, + timestamp: Date.now(), + }); + return jwtToken; + } + } + + const currentToken = await getTokenCache(tokenCacheRedis, KEY); + if (currentToken && Date.now() < currentToken.token.accessTokenExpires) { + return currentToken.token; + } + + // access token has expired, try to update it + return await lockedRefreshAccessToken(token); + }, + async session({ session, token }) { + const extendedToken = token as JWTWithAccessToken; + return { + ...session, + accessToken: extendedToken.accessToken, + accessTokenExpires: extendedToken.accessTokenExpires, + error: extendedToken.error, + user: { + id: assertExists(extendedToken.sub), + name: extendedToken.name, + email: extendedToken.email, + }, + } satisfies CustomSession; + }, + }, +}; + +async function lockedRefreshAccessToken( + token: JWT, +): Promise { + const lockKey = `${token.sub}-refresh`; + + const existingRefresh = refreshLocks.get(lockKey); + if (existingRefresh) { + return await existingRefresh; + } + + const refreshPromise = (async () => { + try { + const cached = await getTokenCache(tokenCacheRedis, `token:${token.sub}`); + if (cached) { + if (Date.now() - cached.timestamp > TOKEN_CACHE_TTL) { + await deleteTokenCache(tokenCacheRedis, `token:${token.sub}`); + } else if (Date.now() < cached.token.accessTokenExpires) { + return cached.token; + } + } + + const currentToken = cached?.token || (token as JWTWithAccessToken); + const newToken = await refreshAccessToken(currentToken); + + await setTokenCache(tokenCacheRedis, `token:${token.sub}`, { + token: newToken, + timestamp: Date.now(), + }); + + return newToken; + } finally { + setTimeout(() => refreshLocks.delete(lockKey), 100); + } + })(); + + refreshLocks.set(lockKey, refreshPromise); + return refreshPromise; +} + +async function refreshAccessToken(token: JWT): Promise { + try { + const url = `${process.env.AUTHENTIK_REFRESH_TOKEN_URL}`; + + const options = { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + client_id: process.env.AUTHENTIK_CLIENT_ID as string, + client_secret: process.env.AUTHENTIK_CLIENT_SECRET as string, + grant_type: "refresh_token", + refresh_token: token.refreshToken as string, + }).toString(), + method: "POST", + }; + + const response = await fetch(url, options); + if (!response.ok) { + console.error( + new Date().toISOString(), + "Failed to refresh access token. Response status:", + response.status, + ); + const responseBody = await response.text(); + console.error(new Date().toISOString(), "Response body:", responseBody); + throw new Error(`Failed to refresh access token: ${response.statusText}`); + } + const refreshedTokens = await response.json(); + return { + ...token, + accessToken: refreshedTokens.access_token, + accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000, + refreshToken: refreshedTokens.refresh_token, + }; + } catch (error) { + console.error("Error refreshing access token", error); + return { + ...token, + error: REFRESH_ACCESS_TOKEN_ERROR, + } as JWTWithAccessToken; + } +} diff --git a/www/app/lib/edgeConfig.ts b/www/app/lib/edgeConfig.ts index 2e31e146..f234a2cf 100644 --- a/www/app/lib/edgeConfig.ts +++ b/www/app/lib/edgeConfig.ts @@ -1,5 +1,5 @@ import { get } from "@vercel/edge-config"; -import { isDevelopment } from "./utils"; +import { isBuildPhase } from "./next"; type EdgeConfig = { [domainWithDash: string]: { @@ -29,12 +29,18 @@ export function edgeDomainToKey(domain: string) { // get edge config server-side (prefer DomainContext when available), domain is the hostname export async function getConfig() { - const domain = new URL(process.env.NEXT_PUBLIC_SITE_URL!).hostname; - if (process.env.NEXT_PUBLIC_ENV === "development") { - return require("../../config").localConfig; + try { + return require("../../config").localConfig; + } catch (e) { + // next build() WILL try to execute the require above even if conditionally protected + // but thank god it at least runs catch{} block properly + if (!isBuildPhase) throw new Error(e); + return require("../../config-template").localConfig; + } } + const domain = new URL(process.env.NEXT_PUBLIC_SITE_URL!).hostname; let config = await get(edgeDomainToKey(domain)); if (typeof config !== "object") { diff --git a/www/app/lib/next.ts b/www/app/lib/next.ts new file mode 100644 index 00000000..91d88bd2 --- /dev/null +++ b/www/app/lib/next.ts @@ -0,0 +1,2 @@ +// next.js tries to run all the lib code during build phase; we don't always want it when e.g. we have connections initialized we don't want to have +export const isBuildPhase = process.env.NEXT_PHASE?.includes("build"); diff --git a/www/app/lib/queryClient.tsx b/www/app/lib/queryClient.tsx new file mode 100644 index 00000000..bd5946e0 --- /dev/null +++ b/www/app/lib/queryClient.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { QueryClient } from "@tanstack/react-query"; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, // 1 minute + gcTime: 5 * 60 * 1000, // 5 minutes (formerly cacheTime) + retry: 1, + refetchOnWindowFocus: false, + }, + mutations: { + retry: 0, + }, + }, +}); diff --git a/www/app/lib/redisClient.ts b/www/app/lib/redisClient.ts new file mode 100644 index 00000000..1be36538 --- /dev/null +++ b/www/app/lib/redisClient.ts @@ -0,0 +1,46 @@ +import Redis from "ioredis"; +import { isBuildPhase } from "./next"; + +export type RedisClient = Pick; + +const getRedisClient = (): RedisClient => { + const redisUrl = process.env.KV_URL; + if (!redisUrl) { + throw new Error("KV_URL environment variable is required"); + } + const redis = new Redis(redisUrl, { + maxRetriesPerRequest: 3, + lazyConnect: true, + }); + + redis.on("error", (error) => { + console.error("Redis error:", error); + }); + + // not necessary but will indicate redis config errors by failfast at startup + // happens only once; after that connection is allowed to die and the lib is assumed to be able to restore it eventually + redis.connect().catch((e) => { + console.error("Failed to connect to Redis:", e); + process.exit(1); + }); + + return redis; +}; + +// next.js buildtime usage - we want to isolate next.js "build" time concepts here +const noopClient: RedisClient = (() => { + const noopSetex: Redis["setex"] = async () => { + return "OK" as const; + }; + const noopDel: Redis["del"] = async () => { + return 0; + }; + return { + get: async () => { + return null; + }, + setex: noopSetex, + del: noopDel, + }; +})(); +export const tokenCacheRedis = isBuildPhase ? noopClient : getRedisClient(); diff --git a/www/app/lib/redisTokenCache.ts b/www/app/lib/redisTokenCache.ts new file mode 100644 index 00000000..4fa4e304 --- /dev/null +++ b/www/app/lib/redisTokenCache.ts @@ -0,0 +1,61 @@ +import { z } from "zod"; +import { REFRESH_ACCESS_TOKEN_BEFORE } from "./auth"; + +const TokenCacheEntrySchema = z.object({ + token: z.object({ + sub: z.string().optional(), + name: z.string().nullish(), + email: z.string().nullish(), + accessToken: z.string(), + accessTokenExpires: z.number(), + refreshToken: z.string().optional(), + error: z.string().optional(), + }), + timestamp: z.number(), +}); + +const TokenCacheEntryCodec = z.codec(z.string(), TokenCacheEntrySchema, { + decode: (jsonString) => { + const parsed = JSON.parse(jsonString); + return TokenCacheEntrySchema.parse(parsed); + }, + encode: (value) => JSON.stringify(value), +}); + +export type TokenCacheEntry = z.infer; + +export type KV = { + get(key: string): Promise; + setex(key: string, seconds: number, value: string): Promise<"OK">; + del(key: string): Promise; +}; + +export async function getTokenCache( + redis: KV, + key: string, +): Promise { + const data = await redis.get(key); + if (!data) return null; + + try { + return TokenCacheEntryCodec.decode(data); + } catch (error) { + console.error("Invalid token cache data:", error); + await redis.del(key); + return null; + } +} + +export async function setTokenCache( + redis: KV, + key: string, + value: TokenCacheEntry, +): Promise { + const encodedValue = TokenCacheEntryCodec.encode(value); + const ttlSeconds = Math.floor(REFRESH_ACCESS_TOKEN_BEFORE / 1000); + await redis.setex(key, ttlSeconds, encodedValue); +} + +export async function deleteTokenCache(redis: KV, key: string): Promise { + await redis.del(key); +} diff --git a/www/app/lib/types.ts b/www/app/lib/types.ts index 851ee5be..0576e186 100644 --- a/www/app/lib/types.ts +++ b/www/app/lib/types.ts @@ -1,10 +1,11 @@ -import { Session } from "next-auth"; -import { JWT } from "next-auth/jwt"; +import type { Session } from "next-auth"; +import type { JWT } from "next-auth/jwt"; +import { parseMaybeNonEmptyString } from "./utils"; export interface JWTWithAccessToken extends JWT { accessToken: string; accessTokenExpires: number; - refreshToken: string; + refreshToken?: string; error?: string; } @@ -12,9 +13,62 @@ export interface CustomSession extends Session { accessToken: string; accessTokenExpires: number; error?: string; - user: { - id?: string; - name?: string | null; - email?: string | null; + user: Session["user"] & { + id: string; }; } + +// assumption that JWT is JWTWithAccessToken - we set it in jwt callback of auth; typing isn't strong around there +// but the assumption is crucial to auth working +export const assertExtendedToken = ( + t: T, +): T & { + accessTokenExpires: number; + accessToken: string; +} => { + if ( + typeof (t as { accessTokenExpires: any }).accessTokenExpires === "number" && + !isNaN((t as { accessTokenExpires: any }).accessTokenExpires) && + typeof ( + t as { + accessToken: any; + } + ).accessToken === "string" && + parseMaybeNonEmptyString((t as { accessToken: any }).accessToken) !== null + ) { + return t as T & { + accessTokenExpires: number; + accessToken: string; + }; + } + throw new Error("Token is not extended with access token"); +}; + +export const assertExtendedTokenAndUserId = ( + t: T, +): T & { + accessTokenExpires: number; + accessToken: string; + user: U & { + id: string; + }; +} => { + const extendedToken = assertExtendedToken(t); + if (typeof (extendedToken.user as any)?.id === "string") { + return t as T & { + accessTokenExpires: number; + accessToken: string; + user: U & { + id: string; + }; + }; + } + throw new Error("Token is not extended with user id"); +}; + +// best attempt to check the session is valid +export const assertCustomSession = (s: S): CustomSession => { + const r = assertExtendedTokenAndUserId(s); + // no other checks for now + return r as CustomSession; +}; diff --git a/www/app/lib/useApi.ts b/www/app/lib/useApi.ts deleted file mode 100644 index 837ef84f..00000000 --- a/www/app/lib/useApi.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useSession, signOut } from "next-auth/react"; -import { useContext, useEffect, useState } from "react"; -import { DomainContext, featureEnabled } from "../domainContext"; -import { OpenApi, DefaultService } from "../api"; -import { CustomSession } from "./types"; -import useSessionStatus from "./useSessionStatus"; -import useSessionAccessToken from "./useSessionAccessToken"; - -export default function useApi(): DefaultService | null { - const api_url = useContext(DomainContext).api_url; - const [api, setApi] = useState(null); - const { isLoading, isAuthenticated } = useSessionStatus(); - const { accessToken, error } = useSessionAccessToken(); - - if (!api_url) throw new Error("no API URL"); - - useEffect(() => { - if (error === "RefreshAccessTokenError") { - signOut(); - } - }, [error]); - - useEffect(() => { - if (isLoading || (isAuthenticated && !accessToken)) { - return; - } - - const openApi = new OpenApi({ - BASE: api_url, - TOKEN: accessToken || undefined, - }); - - setApi(openApi); - }, [isLoading, isAuthenticated, accessToken]); - - return api?.default ?? null; -} diff --git a/www/app/lib/useLoginRequiredPages.ts b/www/app/lib/useLoginRequiredPages.ts new file mode 100644 index 00000000..37ee96b1 --- /dev/null +++ b/www/app/lib/useLoginRequiredPages.ts @@ -0,0 +1,26 @@ +// for paths that are not supposed to be public +import { PROTECTED_PAGES } from "./auth"; +import { usePathname } from "next/navigation"; +import { useAuth } from "./AuthProvider"; +import { useEffect } from "react"; + +const HOME = "/" as const; + +export const useLoginRequiredPages = () => { + const pathname = usePathname(); + const isProtected = PROTECTED_PAGES.test(pathname); + const auth = useAuth(); + const isNotLoggedIn = auth.status === "unauthenticated"; + // safety + const isLastDestination = pathname === HOME; + const shouldRedirect = isNotLoggedIn && isProtected && !isLastDestination; + useEffect(() => { + if (!shouldRedirect) return; + // on the backend, the redirect goes straight to the auth provider, but we don't have it because it's hidden inside next-auth middleware + // so we just "softly" lead the user to the main page + // warning: if HOME redirects somewhere else, we won't be protected by isLastDestination + window.location.href = HOME; + }, [shouldRedirect]); + // optionally save from blink, since window.location.href takes a bit of time + return shouldRedirect ? HOME : null; +}; diff --git a/www/app/lib/useSessionAccessToken.ts b/www/app/lib/useSessionAccessToken.ts deleted file mode 100644 index fc28c076..00000000 --- a/www/app/lib/useSessionAccessToken.ts +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { useSession as useNextAuthSession } from "next-auth/react"; -import { CustomSession } from "./types"; - -export default function useSessionAccessToken() { - const { data: session } = useNextAuthSession(); - const customSession = session as CustomSession; - const naAccessToken = customSession?.accessToken; - const naAccessTokenExpires = customSession?.accessTokenExpires; - const naError = customSession?.error; - const [accessToken, setAccessToken] = useState(null); - const [accessTokenExpires, setAccessTokenExpires] = useState( - null, - ); - const [error, setError] = useState(); - - useEffect(() => { - if (naAccessToken !== accessToken) { - setAccessToken(naAccessToken); - } - }, [naAccessToken]); - - useEffect(() => { - if (naAccessTokenExpires !== accessTokenExpires) { - setAccessTokenExpires(naAccessTokenExpires); - } - }, [naAccessTokenExpires]); - - useEffect(() => { - if (naError !== error) { - setError(naError); - } - }, [naError]); - - return { - accessToken, - accessTokenExpires, - error, - }; -} diff --git a/www/app/lib/useSessionStatus.ts b/www/app/lib/useSessionStatus.ts deleted file mode 100644 index 5629c025..00000000 --- a/www/app/lib/useSessionStatus.ts +++ /dev/null @@ -1,22 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { useSession as useNextAuthSession } from "next-auth/react"; -import { Session } from "next-auth"; - -export default function useSessionStatus() { - const { status: naStatus } = useNextAuthSession(); - const [status, setStatus] = useState("loading"); - - useEffect(() => { - if (naStatus !== "loading" && naStatus !== status) { - setStatus(naStatus); - } - }, [naStatus]); - - return { - status, - isLoading: status === "loading", - isAuthenticated: status === "authenticated", - }; -} diff --git a/www/app/lib/useSessionUser.ts b/www/app/lib/useSessionUser.ts deleted file mode 100644 index 2da299f5..00000000 --- a/www/app/lib/useSessionUser.ts +++ /dev/null @@ -1,33 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { useSession as useNextAuthSession } from "next-auth/react"; -import { Session } from "next-auth"; - -// user type with id, name, email -export interface User { - id?: string | null; - name?: string | null; - email?: string | null; -} - -export default function useSessionUser() { - const { data: session } = useNextAuthSession(); - const [user, setUser] = useState(null); - - useEffect(() => { - if (!session?.user) { - setUser(null); - return; - } - if (JSON.stringify(session.user) !== JSON.stringify(user)) { - setUser(session.user); - } - }, [session]); - - return { - id: user?.id, - name: user?.name, - email: user?.email, - }; -} diff --git a/www/app/lib/useUserName.ts b/www/app/lib/useUserName.ts new file mode 100644 index 00000000..80814281 --- /dev/null +++ b/www/app/lib/useUserName.ts @@ -0,0 +1,7 @@ +import { useAuth } from "./AuthProvider"; + +export const useUserName = (): string | null | undefined => { + const auth = useAuth(); + if (auth.status !== "authenticated") return undefined; + return auth.user?.name || null; +}; diff --git a/www/app/lib/utils.ts b/www/app/lib/utils.ts index 80d0d91b..122ab234 100644 --- a/www/app/lib/utils.ts +++ b/www/app/lib/utils.ts @@ -137,9 +137,28 @@ export function extractDomain(url) { } } -export function assertExists(value: T | null | undefined, err?: string): T { +export type NonEmptyString = string & { __brand: "NonEmptyString" }; +export const parseMaybeNonEmptyString = ( + s: string, + trim = true, +): NonEmptyString | null => { + s = trim ? s.trim() : s; + return s.length > 0 ? (s as NonEmptyString) : null; +}; +export const parseNonEmptyString = (s: string, trim = true): NonEmptyString => + assertExists(parseMaybeNonEmptyString(s, trim), "Expected non-empty string"); + +export const assertExists = ( + value: T | null | undefined, + err?: string, +): T => { if (value === null || value === undefined) { throw new Error(`Assertion failed: ${err ?? "value is null or undefined"}`); } return value; -} +}; + +export const assertExistsAndNonEmptyString = ( + value: string | null | undefined, +): NonEmptyString => + parseNonEmptyString(assertExists(value, "Expected non-empty string")); diff --git a/www/app/providers.tsx b/www/app/providers.tsx index f0f1ea52..2e3b78eb 100644 --- a/www/app/providers.tsx +++ b/www/app/providers.tsx @@ -6,16 +6,26 @@ import system from "./styles/theme"; import { WherebyProvider } from "@whereby.com/browser-sdk/react"; import { Toaster } from "./components/ui/toaster"; import { NuqsAdapter } from "nuqs/adapters/next/app"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { queryClient } from "./lib/queryClient"; +import { AuthProvider } from "./lib/AuthProvider"; +import { SessionProvider as SessionProviderNextAuth } from "next-auth/react"; export function Providers({ children }: { children: React.ReactNode }) { return ( - - - {children} - - - + + + + + + {children} + + + + + + ); } diff --git a/www/app/reflector-api.d.ts b/www/app/reflector-api.d.ts new file mode 100644 index 00000000..8a2cadb0 --- /dev/null +++ b/www/app/reflector-api.d.ts @@ -0,0 +1,2330 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/metrics": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Metrics + * @description Endpoint that serves Prometheus metrics. + */ + get: operations["metrics"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/meetings/{meeting_id}/consent": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Meeting Audio Consent */ + post: operations["v1_meeting_audio_consent"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/rooms": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Rooms List */ + get: operations["v1_rooms_list"]; + put?: never; + /** Rooms Create */ + post: operations["v1_rooms_create"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/rooms/{room_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Rooms Get */ + get: operations["v1_rooms_get"]; + put?: never; + post?: never; + /** Rooms Delete */ + delete: operations["v1_rooms_delete"]; + options?: never; + head?: never; + /** Rooms Update */ + patch: operations["v1_rooms_update"]; + trace?: never; + }; + "/v1/rooms/{room_name}/meeting": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Rooms Create Meeting */ + post: operations["v1_rooms_create_meeting"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/rooms/{room_id}/webhook/test": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Rooms Test Webhook + * @description Test webhook configuration by sending a sample payload. + */ + post: operations["v1_rooms_test_webhook"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Transcripts List */ + get: operations["v1_transcripts_list"]; + put?: never; + /** Transcripts Create */ + post: operations["v1_transcripts_create"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/search": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Transcripts Search + * @description Full-text search across transcript titles and content. + */ + get: operations["v1_transcripts_search"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Transcript Get */ + get: operations["v1_transcript_get"]; + put?: never; + post?: never; + /** Transcript Delete */ + delete: operations["v1_transcript_delete"]; + options?: never; + head?: never; + /** Transcript Update */ + patch: operations["v1_transcript_update"]; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/topics": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Transcript Get Topics */ + get: operations["v1_transcript_get_topics"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/topics/with-words": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Transcript Get Topics With Words */ + get: operations["v1_transcript_get_topics_with_words"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/topics/{topic_id}/words-per-speaker": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Transcript Get Topics With Words Per Speaker */ + get: operations["v1_transcript_get_topics_with_words_per_speaker"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/zulip": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Transcript Post To Zulip */ + post: operations["v1_transcript_post_to_zulip"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/audio/mp3": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Transcript Get Audio Mp3 */ + get: operations["v1_transcript_get_audio_mp3"]; + put?: never; + post?: never; + delete?: never; + options?: never; + /** Transcript Get Audio Mp3 */ + head: operations["v1_transcript_head_audio_mp3"]; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/audio/waveform": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Transcript Get Audio Waveform */ + get: operations["v1_transcript_get_audio_waveform"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/participants": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Transcript Get Participants */ + get: operations["v1_transcript_get_participants"]; + put?: never; + /** Transcript Add Participant */ + post: operations["v1_transcript_add_participant"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/participants/{participant_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Transcript Get Participant */ + get: operations["v1_transcript_get_participant"]; + put?: never; + post?: never; + /** Transcript Delete Participant */ + delete: operations["v1_transcript_delete_participant"]; + options?: never; + head?: never; + /** Transcript Update Participant */ + patch: operations["v1_transcript_update_participant"]; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/speaker/assign": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** Transcript Assign Speaker */ + patch: operations["v1_transcript_assign_speaker"]; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/speaker/merge": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** Transcript Merge Speaker */ + patch: operations["v1_transcript_merge_speaker"]; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/record/upload": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Transcript Record Upload */ + post: operations["v1_transcript_record_upload"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/events": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Transcript Get Websocket Events */ + get: operations["v1_transcript_get_websocket_events"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/record/webrtc": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Transcript Record Webrtc */ + post: operations["v1_transcript_record_webrtc"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/transcripts/{transcript_id}/process": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Transcript Process */ + post: operations["v1_transcript_process"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** User Me */ + get: operations["v1_user_me"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/zulip/streams": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Zulip Get Streams + * @description Get all Zulip streams. + */ + get: operations["v1_zulip_get_streams"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/zulip/streams/{stream_id}/topics": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Zulip Get Topics + * @description Get all topics for a specific Zulip stream. + */ + get: operations["v1_zulip_get_topics"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/whereby": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Whereby Webhook */ + post: operations["v1_whereby_webhook"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** AudioWaveform */ + AudioWaveform: { + /** Data */ + data: number[]; + }; + /** Body_transcript_record_upload_v1_transcripts__transcript_id__record_upload_post */ + Body_transcript_record_upload_v1_transcripts__transcript_id__record_upload_post: { + /** + * Chunk + * Format: binary + */ + chunk: string; + }; + /** CreateParticipant */ + CreateParticipant: { + /** Speaker */ + speaker?: number | null; + /** Name */ + name: string; + }; + /** CreateRoom */ + CreateRoom: { + /** Name */ + name: string; + /** Zulip Auto Post */ + zulip_auto_post: boolean; + /** Zulip Stream */ + zulip_stream: string; + /** Zulip Topic */ + zulip_topic: string; + /** Is Locked */ + is_locked: boolean; + /** Room Mode */ + room_mode: string; + /** Recording Type */ + recording_type: string; + /** Recording Trigger */ + recording_trigger: string; + /** Is Shared */ + is_shared: boolean; + /** Webhook Url */ + webhook_url: string; + /** Webhook Secret */ + webhook_secret: string; + }; + /** CreateTranscript */ + CreateTranscript: { + /** Name */ + name: string; + /** + * Source Language + * @default en + */ + source_language: string; + /** + * Target Language + * @default en + */ + target_language: string; + source_kind?: components["schemas"]["SourceKind"] | null; + }; + /** DeletionStatus */ + DeletionStatus: { + /** Status */ + status: string; + }; + /** GetTranscript */ + GetTranscript: { + /** Id */ + id: string; + /** User Id */ + user_id: string | null; + /** Name */ + name: string; + /** + * Status + * @enum {string} + */ + status: + | "idle" + | "uploaded" + | "recording" + | "processing" + | "error" + | "ended"; + /** Locked */ + locked: boolean; + /** Duration */ + duration: number; + /** Title */ + title: string | null; + /** Short Summary */ + short_summary: string | null; + /** Long Summary */ + long_summary: string | null; + /** Created At */ + created_at: string; + /** + * Share Mode + * @default private + */ + share_mode: string; + /** Source Language */ + source_language: string | null; + /** Target Language */ + target_language: string | null; + /** Reviewed */ + reviewed: boolean; + /** Meeting Id */ + meeting_id: string | null; + source_kind: components["schemas"]["SourceKind"]; + /** Room Id */ + room_id?: string | null; + /** Room Name */ + room_name?: string | null; + /** Audio Deleted */ + audio_deleted?: boolean | null; + /** Participants */ + participants: components["schemas"]["TranscriptParticipant"][] | null; + }; + /** GetTranscriptMinimal */ + GetTranscriptMinimal: { + /** Id */ + id: string; + /** User Id */ + user_id: string | null; + /** Name */ + name: string; + /** + * Status + * @enum {string} + */ + status: + | "idle" + | "uploaded" + | "recording" + | "processing" + | "error" + | "ended"; + /** Locked */ + locked: boolean; + /** Duration */ + duration: number; + /** Title */ + title: string | null; + /** Short Summary */ + short_summary: string | null; + /** Long Summary */ + long_summary: string | null; + /** Created At */ + created_at: string; + /** + * Share Mode + * @default private + */ + share_mode: string; + /** Source Language */ + source_language: string | null; + /** Target Language */ + target_language: string | null; + /** Reviewed */ + reviewed: boolean; + /** Meeting Id */ + meeting_id: string | null; + source_kind: components["schemas"]["SourceKind"]; + /** Room Id */ + room_id?: string | null; + /** Room Name */ + room_name?: string | null; + /** Audio Deleted */ + audio_deleted?: boolean | null; + }; + /** GetTranscriptSegmentTopic */ + GetTranscriptSegmentTopic: { + /** Text */ + text: string; + /** Start */ + start: number; + /** Speaker */ + speaker: number; + }; + /** GetTranscriptTopic */ + GetTranscriptTopic: { + /** Id */ + id: string; + /** Title */ + title: string; + /** Summary */ + summary: string; + /** Timestamp */ + timestamp: number; + /** Duration */ + duration: number | null; + /** Transcript */ + transcript: string; + /** + * Segments + * @default [] + */ + segments: components["schemas"]["GetTranscriptSegmentTopic"][]; + }; + /** GetTranscriptTopicWithWords */ + GetTranscriptTopicWithWords: { + /** Id */ + id: string; + /** Title */ + title: string; + /** Summary */ + summary: string; + /** Timestamp */ + timestamp: number; + /** Duration */ + duration: number | null; + /** Transcript */ + transcript: string; + /** + * Segments + * @default [] + */ + segments: components["schemas"]["GetTranscriptSegmentTopic"][]; + /** + * Words + * @default [] + */ + words: components["schemas"]["Word"][]; + }; + /** GetTranscriptTopicWithWordsPerSpeaker */ + GetTranscriptTopicWithWordsPerSpeaker: { + /** Id */ + id: string; + /** Title */ + title: string; + /** Summary */ + summary: string; + /** Timestamp */ + timestamp: number; + /** Duration */ + duration: number | null; + /** Transcript */ + transcript: string; + /** + * Segments + * @default [] + */ + segments: components["schemas"]["GetTranscriptSegmentTopic"][]; + /** + * Words Per Speaker + * @default [] + */ + words_per_speaker: components["schemas"]["SpeakerWords"][]; + }; + /** HTTPValidationError */ + HTTPValidationError: { + /** Detail */ + detail?: components["schemas"]["ValidationError"][]; + }; + /** Meeting */ + Meeting: { + /** Id */ + id: string; + /** Room Name */ + room_name: string; + /** Room Url */ + room_url: string; + /** Host Room Url */ + host_room_url: string; + /** + * Start Date + * Format: date-time + */ + start_date: string; + /** + * End Date + * Format: date-time + */ + end_date: string; + /** + * Recording Type + * @default cloud + * @enum {string} + */ + recording_type: "none" | "local" | "cloud"; + }; + /** MeetingConsentRequest */ + MeetingConsentRequest: { + /** Consent Given */ + consent_given: boolean; + }; + /** Page[GetTranscriptMinimal] */ + Page_GetTranscriptMinimal_: { + /** Items */ + items: components["schemas"]["GetTranscriptMinimal"][]; + /** Total */ + total?: number | null; + /** Page */ + page: number | null; + /** Size */ + size: number | null; + /** Pages */ + pages?: number | null; + }; + /** Page[RoomDetails] */ + Page_RoomDetails_: { + /** Items */ + items: components["schemas"]["RoomDetails"][]; + /** Total */ + total?: number | null; + /** Page */ + page: number | null; + /** Size */ + size: number | null; + /** Pages */ + pages?: number | null; + }; + /** Participant */ + Participant: { + /** Id */ + id: string; + /** Speaker */ + speaker: number | null; + /** Name */ + name: string; + }; + /** Room */ + Room: { + /** Id */ + id: string; + /** Name */ + name: string; + /** User Id */ + user_id: string; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Zulip Auto Post */ + zulip_auto_post: boolean; + /** Zulip Stream */ + zulip_stream: string; + /** Zulip Topic */ + zulip_topic: string; + /** Is Locked */ + is_locked: boolean; + /** Room Mode */ + room_mode: string; + /** Recording Type */ + recording_type: string; + /** Recording Trigger */ + recording_trigger: string; + /** Is Shared */ + is_shared: boolean; + }; + /** RoomDetails */ + RoomDetails: { + /** Id */ + id: string; + /** Name */ + name: string; + /** User Id */ + user_id: string; + /** + * Created At + * Format: date-time + */ + created_at: string; + /** Zulip Auto Post */ + zulip_auto_post: boolean; + /** Zulip Stream */ + zulip_stream: string; + /** Zulip Topic */ + zulip_topic: string; + /** Is Locked */ + is_locked: boolean; + /** Room Mode */ + room_mode: string; + /** Recording Type */ + recording_type: string; + /** Recording Trigger */ + recording_trigger: string; + /** Is Shared */ + is_shared: boolean; + /** Webhook Url */ + webhook_url: string | null; + /** Webhook Secret */ + webhook_secret: string | null; + }; + /** RtcOffer */ + RtcOffer: { + /** Sdp */ + sdp: string; + /** Type */ + type: string; + }; + /** SearchResponse */ + SearchResponse: { + /** Results */ + results: components["schemas"]["SearchResult"][]; + /** + * Total + * @description Total number of search results + */ + total: number; + /** Query */ + query?: string | null; + /** + * Limit + * @description Results per page + */ + limit: number; + /** + * Offset + * @description Number of results to skip + */ + offset: number; + }; + /** + * SearchResult + * @description Public search result model with computed fields. + */ + SearchResult: { + /** Id */ + id: string; + /** Title */ + title?: string | null; + /** User Id */ + user_id?: string | null; + /** Room Id */ + room_id?: string | null; + /** Room Name */ + room_name?: string | null; + source_kind: components["schemas"]["SourceKind"]; + /** Created At */ + created_at: string; + /** Status */ + status: string; + /** Rank */ + rank: number; + /** + * Duration + * @description Duration in seconds + */ + duration: number | null; + /** + * Search Snippets + * @description Text snippets around search matches + */ + search_snippets: string[]; + /** + * Total Match Count + * @description Total number of matches found in the transcript + * @default 0 + */ + total_match_count: number; + }; + /** + * SourceKind + * @enum {string} + */ + SourceKind: "room" | "live" | "file"; + /** SpeakerAssignment */ + SpeakerAssignment: { + /** Speaker */ + speaker?: number | null; + /** Participant */ + participant?: string | null; + /** Timestamp From */ + timestamp_from: number; + /** Timestamp To */ + timestamp_to: number; + }; + /** SpeakerAssignmentStatus */ + SpeakerAssignmentStatus: { + /** Status */ + status: string; + }; + /** SpeakerMerge */ + SpeakerMerge: { + /** Speaker From */ + speaker_from: number; + /** Speaker To */ + speaker_to: number; + }; + /** SpeakerWords */ + SpeakerWords: { + /** Speaker */ + speaker: number; + /** Words */ + words: components["schemas"]["Word"][]; + }; + /** Stream */ + Stream: { + /** Stream Id */ + stream_id: number; + /** Name */ + name: string; + }; + /** Topic */ + Topic: { + /** Name */ + name: string; + }; + /** TranscriptParticipant */ + TranscriptParticipant: { + /** Id */ + id?: string; + /** Speaker */ + speaker: number | null; + /** Name */ + name: string; + }; + /** UpdateParticipant */ + UpdateParticipant: { + /** Speaker */ + speaker?: number | null; + /** Name */ + name?: string | null; + }; + /** UpdateRoom */ + UpdateRoom: { + /** Name */ + name: string; + /** Zulip Auto Post */ + zulip_auto_post: boolean; + /** Zulip Stream */ + zulip_stream: string; + /** Zulip Topic */ + zulip_topic: string; + /** Is Locked */ + is_locked: boolean; + /** Room Mode */ + room_mode: string; + /** Recording Type */ + recording_type: string; + /** Recording Trigger */ + recording_trigger: string; + /** Is Shared */ + is_shared: boolean; + /** Webhook Url */ + webhook_url: string; + /** Webhook Secret */ + webhook_secret: string; + }; + /** UpdateTranscript */ + UpdateTranscript: { + /** Name */ + name?: string | null; + /** Locked */ + locked?: boolean | null; + /** Title */ + title?: string | null; + /** Short Summary */ + short_summary?: string | null; + /** Long Summary */ + long_summary?: string | null; + /** Share Mode */ + share_mode?: ("public" | "semi-private" | "private") | null; + /** Participants */ + participants?: components["schemas"]["TranscriptParticipant"][] | null; + /** Reviewed */ + reviewed?: boolean | null; + /** Audio Deleted */ + audio_deleted?: boolean | null; + }; + /** UserInfo */ + UserInfo: { + /** Sub */ + sub: string; + /** Email */ + email: string | null; + /** Email Verified */ + email_verified: boolean | null; + }; + /** ValidationError */ + ValidationError: { + /** Location */ + loc: (string | number)[]; + /** Message */ + msg: string; + /** Error Type */ + type: string; + }; + /** WebhookTestResult */ + WebhookTestResult: { + /** Success */ + success: boolean; + /** + * Message + * @default + */ + message: string; + /** + * Error + * @default + */ + error: string; + /** Status Code */ + status_code?: number | null; + /** Response Preview */ + response_preview?: string | null; + }; + /** WherebyWebhookEvent */ + WherebyWebhookEvent: { + /** Apiversion */ + apiVersion: string; + /** Id */ + id: string; + /** + * Createdat + * Format: date-time + */ + createdAt: string; + /** Type */ + type: string; + /** Data */ + data: { + [key: string]: unknown; + }; + }; + /** Word */ + Word: { + /** Text */ + text: string; + /** + * Start + * @description Time in seconds with float part + */ + start: number; + /** + * End + * @description Time in seconds with float part + */ + end: number; + /** + * Speaker + * @default 0 + */ + speaker: number; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + metrics: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + }; + }; + v1_meeting_audio_consent: { + parameters: { + query?: never; + header?: never; + path: { + meeting_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["MeetingConsentRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_rooms_list: { + parameters: { + query?: { + /** @description Page number */ + page?: number; + /** @description Page size */ + size?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Page_RoomDetails_"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_rooms_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateRoom"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Room"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_rooms_get: { + parameters: { + query?: never; + header?: never; + path: { + room_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RoomDetails"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_rooms_delete: { + parameters: { + query?: never; + header?: never; + path: { + room_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeletionStatus"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_rooms_update: { + parameters: { + query?: never; + header?: never; + path: { + room_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateRoom"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["RoomDetails"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_rooms_create_meeting: { + parameters: { + query?: never; + header?: never; + path: { + room_name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Meeting"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_rooms_test_webhook: { + parameters: { + query?: never; + header?: never; + path: { + room_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WebhookTestResult"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcripts_list: { + parameters: { + query?: { + source_kind?: components["schemas"]["SourceKind"] | null; + room_id?: string | null; + search_term?: string | null; + /** @description Page number */ + page?: number; + /** @description Page size */ + size?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Page_GetTranscriptMinimal_"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcripts_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateTranscript"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetTranscript"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcripts_search: { + parameters: { + query: { + /** @description Search query text */ + q: string; + /** @description Results per page */ + limit?: number; + /** @description Number of results to skip */ + offset?: number; + room_id?: string | null; + source_kind?: components["schemas"]["SourceKind"] | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SearchResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_get: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetTranscript"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_delete: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeletionStatus"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_update: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateTranscript"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetTranscript"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_get_topics: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetTranscriptTopic"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_get_topics_with_words: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetTranscriptTopicWithWords"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_get_topics_with_words_per_speaker: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + topic_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GetTranscriptTopicWithWordsPerSpeaker"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_post_to_zulip: { + parameters: { + query: { + stream: string; + topic: string; + include_topics: boolean; + }; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_get_audio_mp3: { + parameters: { + query?: { + token?: string | null; + }; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_head_audio_mp3: { + parameters: { + query?: { + token?: string | null; + }; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_get_audio_waveform: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AudioWaveform"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_get_participants: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Participant"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_add_participant: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateParticipant"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Participant"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_get_participant: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + participant_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Participant"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_delete_participant: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + participant_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeletionStatus"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_update_participant: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + participant_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateParticipant"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Participant"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_assign_speaker: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SpeakerAssignment"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SpeakerAssignmentStatus"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_merge_speaker: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SpeakerMerge"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SpeakerAssignmentStatus"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_record_upload: { + parameters: { + query: { + chunk_number: number; + total_chunks: number; + }; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "multipart/form-data": components["schemas"]["Body_transcript_record_upload_v1_transcripts__transcript_id__record_upload_post"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_get_websocket_events: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_record_webrtc: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["RtcOffer"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_transcript_process: { + parameters: { + query?: never; + header?: never; + path: { + transcript_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_user_me: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserInfo"] | null; + }; + }; + }; + }; + v1_zulip_get_streams: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Stream"][]; + }; + }; + }; + }; + v1_zulip_get_topics: { + parameters: { + query?: never; + header?: never; + path: { + stream_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Topic"][]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + v1_whereby_webhook: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["WherebyWebhookEvent"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; +} diff --git a/www/jest.config.js b/www/jest.config.js new file mode 100644 index 00000000..d2f3247b --- /dev/null +++ b/www/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + preset: "ts-jest", + testEnvironment: "node", + roots: ["/app"], + testMatch: ["**/__tests__/**/*.test.ts"], + collectCoverage: true, + collectCoverageFrom: ["app/**/*.ts", "!app/**/*.d.ts"], +}; diff --git a/www/middleware.ts b/www/middleware.ts index 39145220..2b60d715 100644 --- a/www/middleware.ts +++ b/www/middleware.ts @@ -1,16 +1,7 @@ import { withAuth } from "next-auth/middleware"; import { getConfig } from "./app/lib/edgeConfig"; import { NextResponse } from "next/server"; - -const LOGIN_REQUIRED_PAGES = [ - "/transcripts/[!new]", - "/browse(.*)", - "/rooms(.*)", -]; - -const PROTECTED_PAGES = new RegExp( - LOGIN_REQUIRED_PAGES.map((page) => `^${page}$`).join("|"), -); +import { PROTECTED_PAGES } from "./app/lib/auth"; export const config = { matcher: [ diff --git a/www/next.config.js b/www/next.config.js index e37d5402..bbc3f710 100644 --- a/www/next.config.js +++ b/www/next.config.js @@ -2,6 +2,9 @@ const nextConfig = { output: "standalone", experimental: { esmExternals: "loose" }, + env: { + IS_CI: process.env.IS_CI, + }, }; module.exports = nextConfig; diff --git a/www/openapi-ts.config.ts b/www/openapi-ts.config.ts deleted file mode 100644 index 9304b8f7..00000000 --- a/www/openapi-ts.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig } from "@hey-api/openapi-ts"; - -export default defineConfig({ - client: "axios", - name: "OpenApi", - input: "http://127.0.0.1:1250/openapi.json", - output: { - path: "./app/api", - format: "prettier", - }, - services: { - asClass: true, - }, -}); diff --git a/www/package.json b/www/package.json index 482a29f6..b7511147 100644 --- a/www/package.json +++ b/www/package.json @@ -8,7 +8,8 @@ "start": "next start", "lint": "next lint", "format": "prettier --write .", - "openapi": "openapi-ts" + "openapi": "openapi-typescript http://127.0.0.1:1250/openapi.json -o ./app/reflector-api.d.ts", + "test": "jest" }, "dependencies": { "@chakra-ui/react": "^3.24.2", @@ -17,21 +18,24 @@ "@fortawesome/free-solid-svg-icons": "^6.4.0", "@fortawesome/react-fontawesome": "^0.2.0", "@sentry/nextjs": "^7.77.0", + "@tanstack/react-query": "^5.85.9", + "@types/ioredis": "^5.0.0", "@vercel/edge-config": "^0.4.1", - "@vercel/kv": "^2.0.0", "@whereby.com/browser-sdk": "^3.3.4", "autoprefixer": "10.4.20", "axios": "^1.8.2", "eslint": "^9.33.0", "eslint-config-next": "^14.2.31", "fontawesome": "^5.6.3", - "ioredis": "^5.4.1", + "ioredis": "^5.7.0", "jest-worker": "^29.6.2", "lucide-react": "^0.525.0", "next": "^14.2.30", "next-auth": "^4.24.7", "next-themes": "^0.4.6", "nuqs": "^2.4.3", + "openapi-fetch": "^0.14.0", + "openapi-react-query": "^0.5.0", "postcss": "8.4.31", "prop-types": "^15.8.1", "react": "^18.2.0", @@ -41,21 +45,24 @@ "react-markdown": "^9.0.0", "react-qr-code": "^2.0.12", "react-select-search": "^4.1.7", - "redlock": "^5.0.0-beta.2", "sass": "^1.63.6", "simple-peer": "^9.11.1", "tailwindcss": "^3.3.2", "typescript": "^5.1.6", - "wavesurfer.js": "^7.4.2" + "wavesurfer.js": "^7.4.2", + "zod": "^4.1.5" }, "main": "index.js", "repository": "https://github.com/Monadical-SAS/reflector-ui.git", "author": "Andreas ", "license": "All Rights Reserved", "devDependencies": { - "@hey-api/openapi-ts": "^0.48.0", + "@types/jest": "^30.0.0", "@types/react": "18.2.20", + "jest": "^30.1.3", + "openapi-typescript": "^7.9.1", "prettier": "^3.0.0", + "ts-jest": "^29.4.1", "vercel": "^37.3.0" }, "packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748" diff --git a/www/pnpm-lock.yaml b/www/pnpm-lock.yaml index 55aef9c8..14b42c55 100644 --- a/www/pnpm-lock.yaml +++ b/www/pnpm-lock.yaml @@ -24,13 +24,16 @@ importers: version: 0.2.3(@fortawesome/fontawesome-svg-core@6.7.2)(react@18.3.1) "@sentry/nextjs": specifier: ^7.77.0 - version: 7.120.4(next@14.2.31(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(react@18.3.1) + version: 7.120.4(next@14.2.31(@babel/core@7.28.3)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(react@18.3.1) + "@tanstack/react-query": + specifier: ^5.85.9 + version: 5.85.9(react@18.3.1) + "@types/ioredis": + specifier: ^5.0.0 + version: 5.0.0 "@vercel/edge-config": specifier: ^0.4.1 version: 0.4.1 - "@vercel/kv": - specifier: ^2.0.0 - version: 2.0.0 "@whereby.com/browser-sdk": specifier: ^3.3.4 version: 3.13.1(@types/react@18.2.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -50,7 +53,7 @@ importers: specifier: ^5.6.3 version: 5.6.3 ioredis: - specifier: ^5.4.1 + specifier: ^5.7.0 version: 5.7.0 jest-worker: specifier: ^29.6.2 @@ -60,16 +63,22 @@ importers: version: 0.525.0(react@18.3.1) next: specifier: ^14.2.30 - version: 14.2.31(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0) + version: 14.2.31(@babel/core@7.28.3)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0) next-auth: specifier: ^4.24.7 - version: 4.24.11(next@14.2.31(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 4.24.11(next@14.2.31(@babel/core@7.28.3)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nuqs: specifier: ^2.4.3 - version: 2.4.3(next@14.2.31(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(react@18.3.1) + version: 2.4.3(next@14.2.31(@babel/core@7.28.3)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(react@18.3.1) + openapi-fetch: + specifier: ^0.14.0 + version: 0.14.0 + openapi-react-query: + specifier: ^0.5.0 + version: 0.5.0(@tanstack/react-query@5.85.9(react@18.3.1))(openapi-fetch@0.14.0) postcss: specifier: 8.4.31 version: 8.4.31 @@ -97,9 +106,6 @@ importers: react-select-search: specifier: ^4.1.7 version: 4.1.8(prop-types@15.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - redlock: - specifier: ^5.0.0-beta.2 - version: 5.0.0-beta.2 sass: specifier: ^1.63.6 version: 1.90.0 @@ -115,16 +121,28 @@ importers: wavesurfer.js: specifier: ^7.4.2 version: 7.10.1 + zod: + specifier: ^4.1.5 + version: 4.1.5 devDependencies: - "@hey-api/openapi-ts": - specifier: ^0.48.0 - version: 0.48.3(typescript@5.9.2) + "@types/jest": + specifier: ^30.0.0 + version: 30.0.0 "@types/react": specifier: 18.2.20 version: 18.2.20 + jest: + specifier: ^30.1.3 + version: 30.1.3(@types/node@16.18.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@types/node@16.18.11)(typescript@5.9.2)) + openapi-typescript: + specifier: ^7.9.1 + version: 7.9.1(typescript@5.9.2) prettier: specifier: ^3.0.0 version: 3.6.2 + ts-jest: + specifier: ^29.4.1 + version: 29.4.1(@babel/core@7.28.3)(@jest/transform@30.1.2)(@jest/types@30.0.5)(babel-jest@30.1.2(@babel/core@7.28.3))(jest-util@30.0.5)(jest@30.1.3(@types/node@16.18.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@types/node@16.18.11)(typescript@5.9.2)))(typescript@5.9.2) vercel: specifier: ^37.3.0 version: 37.14.0 @@ -137,12 +155,12 @@ packages: } engines: { node: ">=10" } - "@apidevtools/json-schema-ref-parser@11.6.4": + "@ampproject/remapping@2.3.0": resolution: { - integrity: sha512-9K6xOqeevacvweLGik6LnZCb1fBtCOSIWQs8d096XGeqoLKC33UVMGz9+77Gw44KvbH4pKcQPWo4ZpxkXYj05w==, + integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==, } - engines: { node: ">= 16" } + engines: { node: ">=6.0.0" } "@ark-ui/react@5.18.2": resolution: @@ -160,6 +178,20 @@ packages: } engines: { node: ">=6.9.0" } + "@babel/compat-data@7.28.0": + resolution: + { + integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==, + } + engines: { node: ">=6.9.0" } + + "@babel/core@7.28.3": + resolution: + { + integrity: sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==, + } + engines: { node: ">=6.9.0" } + "@babel/generator@7.28.0": resolution: { @@ -167,6 +199,20 @@ packages: } engines: { node: ">=6.9.0" } + "@babel/generator@7.28.3": + resolution: + { + integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-compilation-targets@7.27.2": + resolution: + { + integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==, + } + engines: { node: ">=6.9.0" } + "@babel/helper-globals@7.28.0": resolution: { @@ -181,6 +227,22 @@ packages: } engines: { node: ">=6.9.0" } + "@babel/helper-module-transforms@7.28.3": + resolution: + { + integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==, + } + engines: { node: ">=6.9.0" } + peerDependencies: + "@babel/core": ^7.0.0 + + "@babel/helper-plugin-utils@7.27.1": + resolution: + { + integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==, + } + engines: { node: ">=6.9.0" } + "@babel/helper-string-parser@7.27.1": resolution: { @@ -195,6 +257,20 @@ packages: } engines: { node: ">=6.9.0" } + "@babel/helper-validator-option@7.27.1": + resolution: + { + integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==, + } + engines: { node: ">=6.9.0" } + + "@babel/helpers@7.28.3": + resolution: + { + integrity: sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==, + } + engines: { node: ">=6.9.0" } + "@babel/parser@7.28.0": resolution: { @@ -203,6 +279,156 @@ packages: engines: { node: ">=6.0.0" } hasBin: true + "@babel/parser@7.28.3": + resolution: + { + integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==, + } + engines: { node: ">=6.0.0" } + hasBin: true + + "@babel/plugin-syntax-async-generators@7.8.4": + resolution: + { + integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==, + } + peerDependencies: + "@babel/core": ^7.0.0-0 + + "@babel/plugin-syntax-bigint@7.8.3": + resolution: + { + integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==, + } + peerDependencies: + "@babel/core": ^7.0.0-0 + + "@babel/plugin-syntax-class-properties@7.12.13": + resolution: + { + integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==, + } + peerDependencies: + "@babel/core": ^7.0.0-0 + + "@babel/plugin-syntax-class-static-block@7.14.5": + resolution: + { + integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==, + } + engines: { node: ">=6.9.0" } + peerDependencies: + "@babel/core": ^7.0.0-0 + + "@babel/plugin-syntax-import-attributes@7.27.1": + resolution: + { + integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==, + } + engines: { node: ">=6.9.0" } + peerDependencies: + "@babel/core": ^7.0.0-0 + + "@babel/plugin-syntax-import-meta@7.10.4": + resolution: + { + integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==, + } + peerDependencies: + "@babel/core": ^7.0.0-0 + + "@babel/plugin-syntax-json-strings@7.8.3": + resolution: + { + integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==, + } + peerDependencies: + "@babel/core": ^7.0.0-0 + + "@babel/plugin-syntax-jsx@7.27.1": + resolution: + { + integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==, + } + engines: { node: ">=6.9.0" } + peerDependencies: + "@babel/core": ^7.0.0-0 + + "@babel/plugin-syntax-logical-assignment-operators@7.10.4": + resolution: + { + integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==, + } + peerDependencies: + "@babel/core": ^7.0.0-0 + + "@babel/plugin-syntax-nullish-coalescing-operator@7.8.3": + resolution: + { + integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==, + } + peerDependencies: + "@babel/core": ^7.0.0-0 + + "@babel/plugin-syntax-numeric-separator@7.10.4": + resolution: + { + integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==, + } + peerDependencies: + "@babel/core": ^7.0.0-0 + + "@babel/plugin-syntax-object-rest-spread@7.8.3": + resolution: + { + integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==, + } + peerDependencies: + "@babel/core": ^7.0.0-0 + + "@babel/plugin-syntax-optional-catch-binding@7.8.3": + resolution: + { + integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==, + } + peerDependencies: + "@babel/core": ^7.0.0-0 + + "@babel/plugin-syntax-optional-chaining@7.8.3": + resolution: + { + integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==, + } + peerDependencies: + "@babel/core": ^7.0.0-0 + + "@babel/plugin-syntax-private-property-in-object@7.14.5": + resolution: + { + integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==, + } + engines: { node: ">=6.9.0" } + peerDependencies: + "@babel/core": ^7.0.0-0 + + "@babel/plugin-syntax-top-level-await@7.14.5": + resolution: + { + integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==, + } + engines: { node: ">=6.9.0" } + peerDependencies: + "@babel/core": ^7.0.0-0 + + "@babel/plugin-syntax-typescript@7.27.1": + resolution: + { + integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==, + } + engines: { node: ">=6.9.0" } + peerDependencies: + "@babel/core": ^7.0.0-0 + "@babel/runtime@7.28.2": resolution: { @@ -224,6 +450,13 @@ packages: } engines: { node: ">=6.9.0" } + "@babel/traverse@7.28.3": + resolution: + { + integrity: sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==, + } + engines: { node: ">=6.9.0" } + "@babel/types@7.28.2": resolution: { @@ -231,6 +464,12 @@ packages: } engines: { node: ">=6.9.0" } + "@bcoe/v8-coverage@0.2.3": + resolution: + { + integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==, + } + "@chakra-ui/react@3.24.2": resolution: { @@ -516,16 +755,6 @@ packages: "@fortawesome/fontawesome-svg-core": ~1 || ~6 || ~7 react: ^16.3 || ^17.0.0 || ^18.0.0 || ^19.0.0 - "@hey-api/openapi-ts@0.48.3": - resolution: - { - integrity: sha512-R53Nr4Gicz77icS+RiH0fwHa9A0uFPtzsjC8SBaGwtOel5ZyxeBbayWE6HhE789hp3dok9pegwWncwwOrr4WFA==, - } - engines: { node: ^18.0.0 || >=20.0.0 } - hasBin: true - peerDependencies: - typescript: ^5.x - "@humanfs/core@0.19.1": resolution: { @@ -573,10 +802,10 @@ packages: integrity: sha512-p+Zh1sb6EfrfVaS86jlHGQ9HA66fJhV9x5LiE5vCbZtXEHAuhcmUZUdZ4WrFpUBfNalr2OkAJI5AcKEQF+Lebw==, } - "@ioredis/commands@1.3.0": + "@ioredis/commands@1.3.1": resolution: { - integrity: sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==, + integrity: sha512-bYtU8avhGIcje3IhvF9aSjsa5URMZBHnwKtOvXsT4sfYy9gppW11gLPT/9oNqlJZD47yPKveQFTAFWpHjKvUoQ==, } "@isaacs/cliui@8.0.2": @@ -586,6 +815,107 @@ packages: } engines: { node: ">=12" } + "@istanbuljs/load-nyc-config@1.1.0": + resolution: + { + integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==, + } + engines: { node: ">=8" } + + "@istanbuljs/schema@0.1.3": + resolution: + { + integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==, + } + engines: { node: ">=8" } + + "@jest/console@30.1.2": + resolution: + { + integrity: sha512-BGMAxj8VRmoD0MoA/jo9alMXSRoqW8KPeqOfEo1ncxnRLatTBCpRoOwlwlEMdudp68Q6WSGwYrrLtTGOh8fLzw==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + "@jest/core@30.1.3": + resolution: + { + integrity: sha512-LIQz7NEDDO1+eyOA2ZmkiAyYvZuo6s1UxD/e2IHldR6D7UYogVq3arTmli07MkENLq6/3JEQjp0mA8rrHHJ8KQ==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + "@jest/diff-sequences@30.0.1": + resolution: + { + integrity: sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + "@jest/environment@30.1.2": + resolution: + { + integrity: sha512-N8t1Ytw4/mr9uN28OnVf0SYE2dGhaIxOVYcwsf9IInBKjvofAjbFRvedvBBlyTYk2knbJTiEjEJ2PyyDIBnd9w==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + "@jest/expect-utils@30.1.2": + resolution: + { + integrity: sha512-HXy1qT/bfdjCv7iC336ExbqqYtZvljrV8odNdso7dWK9bSeHtLlvwWWC3YSybSPL03Gg5rug6WLCZAZFH72m0A==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + "@jest/expect@30.1.2": + resolution: + { + integrity: sha512-tyaIExOwQRCxPCGNC05lIjWJztDwk2gPDNSDGg1zitXJJ8dC3++G/CRjE5mb2wQsf89+lsgAgqxxNpDLiCViTA==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + "@jest/fake-timers@30.1.2": + resolution: + { + integrity: sha512-Beljfv9AYkr9K+ETX9tvV61rJTY706BhBUtiaepQHeEGfe0DbpvUA5Z3fomwc5Xkhns6NWrcFDZn+72fLieUnA==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + "@jest/get-type@30.1.0": + resolution: + { + integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + "@jest/globals@30.1.2": + resolution: + { + integrity: sha512-teNTPZ8yZe3ahbYnvnVRDeOjr+3pu2uiAtNtrEsiMjVPPj+cXd5E/fr8BL7v/T7F31vYdEHrI5cC/2OoO/vM9A==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + "@jest/pattern@30.0.1": + resolution: + { + integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + "@jest/reporters@30.1.3": + resolution: + { + integrity: sha512-VWEQmJWfXMOrzdFEOyGjUEOuVXllgZsoPtEHZzfdNz18RmzJ5nlR6kp8hDdY8dDS1yGOXAY7DHT+AOHIPSBV0w==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + "@jest/schemas@29.6.3": resolution: { @@ -593,6 +923,48 @@ packages: } engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + "@jest/schemas@30.0.5": + resolution: + { + integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + "@jest/snapshot-utils@30.1.2": + resolution: + { + integrity: sha512-vHoMTpimcPSR7OxS2S0V1Cpg8eKDRxucHjoWl5u4RQcnxqQrV3avETiFpl8etn4dqxEGarBeHbIBety/f8mLXw==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + "@jest/source-map@30.0.1": + resolution: + { + integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + "@jest/test-result@30.1.3": + resolution: + { + integrity: sha512-P9IV8T24D43cNRANPPokn7tZh0FAFnYS2HIfi5vK18CjRkTDR9Y3e1BoEcAJnl4ghZZF4Ecda4M/k41QkvurEQ==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + "@jest/test-sequencer@30.1.3": + resolution: + { + integrity: sha512-82J+hzC0qeQIiiZDThh+YUadvshdBswi5nuyXlEmXzrhw5ZQSRHeQ5LpVMD/xc8B3wPePvs6VMzHnntxL+4E3w==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + "@jest/transform@30.1.2": + resolution: + { + integrity: sha512-UYYFGifSgfjujf1Cbd3iU/IQoSd6uwsj8XHj5DSDf5ERDcWMdJOPTkHWXj4U+Z/uMagyOQZ6Vne8C4nRIrCxqA==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + "@jest/types@29.6.3": resolution: { @@ -600,6 +972,13 @@ packages: } engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + "@jest/types@30.0.5": + resolution: + { + integrity: sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + "@jridgewell/gen-mapping@0.3.13": resolution: { @@ -631,12 +1010,6 @@ packages: integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==, } - "@jsdevtools/ono@7.1.3": - resolution: - { - integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==, - } - "@mapbox/node-pre-gyp@1.0.11": resolution: { @@ -914,6 +1287,13 @@ packages: } engines: { node: ">=14" } + "@pkgr/core@0.2.9": + resolution: + { + integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==, + } + engines: { node: ^12.20.0 || ^14.18.0 || >=16.0.0 } + "@radix-ui/primitive@1.1.3": resolution: { @@ -1198,6 +1578,25 @@ packages: integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==, } + "@redocly/ajv@8.11.3": + resolution: + { + integrity: sha512-4P3iZse91TkBiY+Dx5DUgxQ9GXkVJf++cmI0MOyLDxV9b5MUBI4II6ES8zA5JCbO72nKAJxWrw4PUPW+YP3ZDQ==, + } + + "@redocly/config@0.22.2": + resolution: + { + integrity: sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==, + } + + "@redocly/openapi-core@1.34.5": + resolution: + { + integrity: sha512-0EbE8LRbkogtcCXU7liAyC00n9uNG9hJ+eMyHFdUsy9lB/WGqnEBgwjA9q2cyzAVcdTkQqTBBU1XePNnN3OijA==, + } + engines: { node: ">=18.17.0", npm: ">=9.5.0" } + "@reduxjs/toolkit@2.8.2": resolution: { @@ -1382,6 +1781,24 @@ packages: integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==, } + "@sinclair/typebox@0.34.41": + resolution: + { + integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==, + } + + "@sinonjs/commons@3.0.1": + resolution: + { + integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==, + } + + "@sinonjs/fake-timers@13.0.5": + resolution: + { + integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==, + } + "@socket.io/component-emitter@3.1.2": resolution: { @@ -1418,6 +1835,20 @@ packages: integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==, } + "@tanstack/query-core@5.85.9": + resolution: + { + integrity: sha512-5fxb9vwyftYE6KFLhhhDyLr8NO75+Wpu7pmTo+TkwKmMX2oxZDoLwcqGP8ItKSpUMwk3urWgQDZfyWr5Jm9LsQ==, + } + + "@tanstack/react-query@5.85.9": + resolution: + { + integrity: sha512-2T5zgSpcOZXGkH/UObIbIkGmUPQqZqn7esVQFXLOze622h4spgWf5jmvrqAo9dnI13/hyMcNsF1jsoDcb59nJQ==, + } + peerDependencies: + react: ^18 || ^19 + "@tootallnate/once@2.0.0": resolution: { @@ -1461,6 +1892,30 @@ packages: integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==, } + "@types/babel__core@7.20.5": + resolution: + { + integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==, + } + + "@types/babel__generator@7.27.0": + resolution: + { + integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==, + } + + "@types/babel__template@7.4.4": + resolution: + { + integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==, + } + + "@types/babel__traverse@7.28.0": + resolution: + { + integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==, + } + "@types/debug@4.1.12": resolution: { @@ -1491,6 +1946,13 @@ packages: integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==, } + "@types/ioredis@5.0.0": + resolution: + { + integrity: sha512-zJbJ3FVE17CNl5KXzdeSPtdltc4tMT3TzC6fxQS0sQngkbFZ6h+0uTafsRqu+eSLIugf6Yb0Ea0SUuRr42Nk9g==, + } + deprecated: This is a stub types definition. ioredis provides its own type definitions, so you do not need this installed. + "@types/istanbul-lib-coverage@2.0.6": resolution: { @@ -1509,6 +1971,12 @@ packages: integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==, } + "@types/jest@30.0.0": + resolution: + { + integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==, + } + "@types/json-schema@7.0.15": resolution: { @@ -1575,6 +2043,12 @@ packages: integrity: sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==, } + "@types/stack-utils@2.0.3": + resolution: + { + integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==, + } + "@types/ua-parser-js@0.7.39": resolution: { @@ -1888,12 +2362,6 @@ packages: cpu: [x64] os: [win32] - "@upstash/redis@1.35.3": - resolution: - { - integrity: sha512-hSjv66NOuahW3MisRGlSgoszU2uONAY2l5Qo3Sae8OT3/Tng9K+2/cBRuyPBX8egwEGcNNCF9+r0V6grNnhL+w==, - } - "@vercel/build-utils@8.4.12": resolution: { @@ -1950,13 +2418,6 @@ packages: integrity: sha512-IPAVaALuGAzt2apvTtBs5tB+8zZRzn/yG3AGp8dFyCsw/v5YOuk0Q5s8Z3fayLvJbFpjrKtqRNDZzVJBBU3MrQ==, } - "@vercel/kv@2.0.0": - resolution: - { - integrity: sha512-zdVrhbzZBYo5d1Hfn4bKtqCeKf0FuzW8rSHauzQVMUgv1+1JOwof2mWcBuI+YMJy8s0G0oqAUfQ7HgUDzb8EbA==, - } - engines: { node: ">=14.6" } - "@vercel/next@4.3.18": resolution: { @@ -2502,6 +2963,13 @@ packages: } engines: { node: ">= 6.0.0" } + agent-base@7.1.4: + resolution: + { + integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==, + } + engines: { node: ">= 14" } + ajv@6.12.6: resolution: { @@ -2514,6 +2982,20 @@ packages: integrity: sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==, } + ansi-colors@4.1.3: + resolution: + { + integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==, + } + engines: { node: ">=6" } + + ansi-escapes@4.3.2: + resolution: + { + integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==, + } + engines: { node: ">=8" } + ansi-regex@5.0.1: resolution: { @@ -2535,6 +3017,13 @@ packages: } engines: { node: ">=8" } + ansi-styles@5.2.0: + resolution: + { + integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==, + } + engines: { node: ">=10" } + ansi-styles@6.2.1: resolution: { @@ -2587,6 +3076,12 @@ packages: integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==, } + argparse@1.0.10: + resolution: + { + integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==, + } + argparse@2.0.1: resolution: { @@ -2758,6 +3253,29 @@ packages: } engines: { node: ">= 0.4" } + babel-jest@30.1.2: + resolution: + { + integrity: sha512-IQCus1rt9kaSh7PQxLYRY5NmkNrNlU2TpabzwV7T2jljnpdHOcmnYYv8QmE04Li4S3a2Lj8/yXyET5pBarPr6g==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + peerDependencies: + "@babel/core": ^7.11.0 + + babel-plugin-istanbul@7.0.0: + resolution: + { + integrity: sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==, + } + engines: { node: ">=12" } + + babel-plugin-jest-hoist@30.0.1: + resolution: + { + integrity: sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + babel-plugin-macros@3.1.0: resolution: { @@ -2765,6 +3283,23 @@ packages: } engines: { node: ">=10", npm: ">=6" } + babel-preset-current-node-syntax@1.2.0: + resolution: + { + integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==, + } + peerDependencies: + "@babel/core": ^7.0.0 || ^8.0.0-0 + + babel-preset-jest@30.0.1: + resolution: + { + integrity: sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + peerDependencies: + "@babel/core": ^7.11.0 + bail@2.0.2: resolution: { @@ -2823,6 +3358,19 @@ packages: engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 } hasBin: true + bs-logger@0.2.6: + resolution: + { + integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==, + } + engines: { node: ">= 6" } + + bser@2.1.1: + resolution: + { + integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==, + } + btoa@1.2.1: resolution: { @@ -2837,6 +3385,12 @@ packages: integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==, } + buffer-from@1.1.2: + resolution: + { + integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==, + } + buffer@6.0.3: resolution: { @@ -2857,17 +3411,6 @@ packages: } engines: { node: ">= 0.8" } - c12@1.11.1: - resolution: - { - integrity: sha512-KDU0TvSvVdaYcQKQ6iPHATGz/7p/KiVjPg4vQrB6Jg/wX9R0yl5RZxWm9IoZqaIHD2+6PZd81+KMGwRr/lRIUg==, - } - peerDependencies: - magicast: ^0.3.4 - peerDependenciesMeta: - magicast: - optional: true - call-bind-apply-helpers@1.0.2: resolution: { @@ -2903,12 +3446,19 @@ packages: } engines: { node: ">= 6" } - camelcase@8.0.0: + camelcase@5.3.1: resolution: { - integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==, + integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==, } - engines: { node: ">=16" } + engines: { node: ">=6" } + + camelcase@6.3.0: + resolution: + { + integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==, + } + engines: { node: ">=10" } caniuse-lite@1.0.30001734: resolution: @@ -2936,6 +3486,19 @@ packages: } engines: { node: ">=10" } + change-case@5.4.4: + resolution: + { + integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==, + } + + char-regex@1.0.2: + resolution: + { + integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==, + } + engines: { node: ">=10" } + character-entities-html4@2.1.0: resolution: { @@ -3007,11 +3570,12 @@ packages: } engines: { node: ">=8" } - citty@0.1.6: + ci-info@4.3.0: resolution: { - integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==, + integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==, } + engines: { node: ">=8" } cjs-module-lexer@1.2.3: resolution: @@ -3019,6 +3583,12 @@ packages: integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==, } + cjs-module-lexer@2.1.0: + resolution: + { + integrity: sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==, + } + classnames@2.5.1: resolution: { @@ -3031,6 +3601,13 @@ packages: integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==, } + cliui@8.0.1: + resolution: + { + integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==, + } + engines: { node: ">=12" } + clsx@2.1.1: resolution: { @@ -3045,12 +3622,25 @@ packages: } engines: { node: ">=0.10.0" } + co@4.6.0: + resolution: + { + integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==, + } + engines: { iojs: ">= 1.0.0", node: ">= 0.12.0" } + code-block-writer@10.1.1: resolution: { integrity: sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw==, } + collect-v8-coverage@1.0.2: + resolution: + { + integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==, + } + color-convert@2.0.1: resolution: { @@ -3071,6 +3661,12 @@ packages: } hasBin: true + colorette@1.4.0: + resolution: + { + integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==, + } + combined-stream@1.0.8: resolution: { @@ -3084,13 +3680,6 @@ packages: integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==, } - commander@12.1.0: - resolution: - { - integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==, - } - engines: { node: ">=18" } - commander@4.1.1: resolution: { @@ -3110,19 +3699,6 @@ packages: integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==, } - confbox@0.1.8: - resolution: - { - integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==, - } - - consola@3.4.2: - resolution: - { - integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==, - } - engines: { node: ^14.18.0 || >=16.10.0 } - console-control-strings@1.1.0: resolution: { @@ -3149,6 +3725,12 @@ packages: integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==, } + convert-source-map@2.0.0: + resolution: + { + integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, + } + cookie@0.7.2: resolution: { @@ -3270,12 +3852,30 @@ packages: integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==, } + dedent@1.7.0: + resolution: + { + integrity: sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==, + } + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + deep-is@0.1.4: resolution: { integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==, } + deepmerge@4.3.1: + resolution: + { + integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==, + } + engines: { node: ">=0.10.0" } + define-data-property@1.1.4: resolution: { @@ -3290,12 +3890,6 @@ packages: } engines: { node: ">= 0.4" } - defu@6.1.4: - resolution: - { - integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==, - } - delayed-stream@1.0.0: resolution: { @@ -3330,12 +3924,6 @@ packages: } engines: { node: ">=6" } - destr@2.0.5: - resolution: - { - integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==, - } - detect-europe-js@0.1.2: resolution: { @@ -3357,6 +3945,13 @@ packages: } engines: { node: ">=8" } + detect-newline@3.1.0: + resolution: + { + integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==, + } + engines: { node: ">=8" } + detect-node-es@1.1.0: resolution: { @@ -3413,13 +4008,6 @@ packages: integrity: sha512-h7g5eduvnLwowJJPkcB5lNzo8vd/Hx4e3I4IOtLpX0qB2wBiuryGLNa61MeFre4b6gMaQIhegMIZ2I8rQCAJwQ==, } - dotenv@16.6.1: - resolution: - { - integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==, - } - engines: { node: ">=12" } - dunder-proto@1.0.1: resolution: { @@ -3447,6 +4035,13 @@ packages: integrity: sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==, } + emittery@0.13.1: + resolution: + { + integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==, + } + engines: { node: ">=12" } + emoji-regex@8.0.0: resolution: { @@ -3753,6 +4348,13 @@ packages: } engines: { node: ">=6" } + escape-string-regexp@2.0.0: + resolution: + { + integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==, + } + engines: { node: ">=8" } + escape-string-regexp@4.0.0: resolution: { @@ -3899,6 +4501,14 @@ packages: } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + esprima@4.0.1: + resolution: + { + integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==, + } + engines: { node: ">=4" } + hasBin: true + esquery@1.6.0: resolution: { @@ -3973,6 +4583,27 @@ packages: } engines: { node: ^8.12.0 || >=9.7.0 } + execa@5.1.1: + resolution: + { + integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==, + } + engines: { node: ">=10" } + + exit-x@0.2.2: + resolution: + { + integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==, + } + engines: { node: ">= 0.8.0" } + + expect@30.1.2: + resolution: + { + integrity: sha512-xvHszRavo28ejws8FpemjhwswGj4w/BetHIL8cU49u4sGyXDw2+p3YbeDbj6xzlxi6kWTjIRSTJ+9sNXPnF0Zg==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + extend@3.0.2: resolution: { @@ -4022,6 +4653,12 @@ packages: integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==, } + fb-watchman@2.0.2: + resolution: + { + integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==, + } + fd-slicer@1.1.0: resolution: { @@ -4065,6 +4702,13 @@ packages: integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==, } + find-up@4.1.0: + resolution: + { + integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==, + } + engines: { node: ">=8" } + find-up@5.0.0: resolution: { @@ -4213,12 +4857,26 @@ packages: } engines: { node: ">= 4" } + gensync@1.0.0-beta.2: + resolution: + { + integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, + } + engines: { node: ">=6.9.0" } + get-browser-rtc@1.1.0: resolution: { integrity: sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ==, } + get-caller-file@2.0.5: + resolution: + { + integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==, + } + engines: { node: 6.* || 8.* || >= 10.* } + get-intrinsic@1.3.0: resolution: { @@ -4233,6 +4891,13 @@ packages: } engines: { node: ">=6" } + get-package-type@0.1.0: + resolution: + { + integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==, + } + engines: { node: ">=8.0.0" } + get-proto@1.0.1: resolution: { @@ -4247,6 +4912,13 @@ packages: } engines: { node: ">=8" } + get-stream@6.0.1: + resolution: + { + integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==, + } + engines: { node: ">=10" } + get-symbol-description@1.1.0: resolution: { @@ -4260,13 +4932,6 @@ packages: integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==, } - giget@1.2.5: - resolution: - { - integrity: sha512-r1ekGw/Bgpi3HLV3h1MRBIlSAdHoIMklpaQ3OQLFcRw9PwAj2rqigvIbg+dBUI51OxVI2jsEtDywDBjSiuf7Ug==, - } - hasBin: true - glob-parent@5.1.2: resolution: { @@ -4437,6 +5102,12 @@ packages: integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==, } + html-escaper@2.0.2: + resolution: + { + integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==, + } + html-url-attributes@3.0.1: resolution: { @@ -4464,6 +5135,13 @@ packages: } engines: { node: ">= 6" } + https-proxy-agent@7.0.6: + resolution: + { + integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==, + } + engines: { node: ">= 14" } + human-signals@1.1.1: resolution: { @@ -4471,6 +5149,13 @@ packages: } engines: { node: ">=8.12.0" } + human-signals@2.1.0: + resolution: + { + integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==, + } + engines: { node: ">=10.17.0" } + hyperhtml-style@0.1.3: resolution: { @@ -4529,6 +5214,14 @@ packages: } engines: { node: ">=6" } + import-local@3.2.0: + resolution: + { + integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==, + } + engines: { node: ">=8" } + hasBin: true + imurmurhash@0.1.4: resolution: { @@ -4536,6 +5229,13 @@ packages: } engines: { node: ">=0.8.19" } + index-to-position@1.1.0: + resolution: + { + integrity: sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==, + } + engines: { node: ">=18" } + inflight@1.0.6: resolution: { @@ -4709,6 +5409,13 @@ packages: } engines: { node: ">=8" } + is-generator-fn@2.1.0: + resolution: + { + integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==, + } + engines: { node: ">=6" } + is-generator-function@1.1.0: resolution: { @@ -4864,6 +5571,41 @@ packages: integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, } + istanbul-lib-coverage@3.2.2: + resolution: + { + integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==, + } + engines: { node: ">=8" } + + istanbul-lib-instrument@6.0.3: + resolution: + { + integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==, + } + engines: { node: ">=10" } + + istanbul-lib-report@3.0.1: + resolution: + { + integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==, + } + engines: { node: ">=10" } + + istanbul-lib-source-maps@5.0.6: + resolution: + { + integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==, + } + engines: { node: ">=10" } + + istanbul-reports@3.2.0: + resolution: + { + integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==, + } + engines: { node: ">=8" } + iterator.prototype@1.1.5: resolution: { @@ -4884,6 +5626,168 @@ packages: integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==, } + jest-changed-files@30.0.5: + resolution: + { + integrity: sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + jest-circus@30.1.3: + resolution: + { + integrity: sha512-Yf3dnhRON2GJT4RYzM89t/EXIWNxKTpWTL9BfF3+geFetWP4XSvJjiU1vrWplOiUkmq8cHLiwuhz+XuUp9DscA==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + jest-cli@30.1.3: + resolution: + { + integrity: sha512-G8E2Ol3OKch1DEeIBl41NP7OiC6LBhfg25Btv+idcusmoUSpqUkbrneMqbW9lVpI/rCKb/uETidb7DNteheuAQ==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@30.1.3: + resolution: + { + integrity: sha512-M/f7gqdQEPgZNA181Myz+GXCe8jXcJsGjCMXUzRj22FIXsZOyHNte84e0exntOvdPaeh9tA0w+B8qlP2fAezfw==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + peerDependencies: + "@types/node": "*" + esbuild-register: ">=3.4.0" + ts-node: ">=9.0.0" + peerDependenciesMeta: + "@types/node": + optional: true + esbuild-register: + optional: true + ts-node: + optional: true + + jest-diff@30.1.2: + resolution: + { + integrity: sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + jest-docblock@30.0.1: + resolution: + { + integrity: sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + jest-each@30.1.0: + resolution: + { + integrity: sha512-A+9FKzxPluqogNahpCv04UJvcZ9B3HamqpDNWNKDjtxVRYB8xbZLFuCr8JAJFpNp83CA0anGQFlpQna9Me+/tQ==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + jest-environment-node@30.1.2: + resolution: + { + integrity: sha512-w8qBiXtqGWJ9xpJIA98M0EIoq079GOQRQUyse5qg1plShUCQ0Ek1VTTcczqKrn3f24TFAgFtT+4q3aOXvjbsuA==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + jest-haste-map@30.1.0: + resolution: + { + integrity: sha512-JLeM84kNjpRkggcGpQLsV7B8W4LNUWz7oDNVnY1Vjj22b5/fAb3kk3htiD+4Na8bmJmjJR7rBtS2Rmq/NEcADg==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + jest-leak-detector@30.1.0: + resolution: + { + integrity: sha512-AoFvJzwxK+4KohH60vRuHaqXfWmeBATFZpzpmzNmYTtmRMiyGPVhkXpBqxUQunw+dQB48bDf4NpUs6ivVbRv1g==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + jest-matcher-utils@30.1.2: + resolution: + { + integrity: sha512-7ai16hy4rSbDjvPTuUhuV8nyPBd6EX34HkBsBcBX2lENCuAQ0qKCPb/+lt8OSWUa9WWmGYLy41PrEzkwRwoGZQ==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + jest-message-util@30.1.0: + resolution: + { + integrity: sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + jest-mock@30.0.5: + resolution: + { + integrity: sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + jest-pnp-resolver@1.2.3: + resolution: + { + integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==, + } + engines: { node: ">=6" } + peerDependencies: + jest-resolve: "*" + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@30.0.1: + resolution: + { + integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + jest-resolve-dependencies@30.1.3: + resolution: + { + integrity: sha512-DNfq3WGmuRyHRHfEet+Zm3QOmVFtIarUOQHHryKPc0YL9ROfgWZxl4+aZq/VAzok2SS3gZdniP+dO4zgo59hBg==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + jest-resolve@30.1.3: + resolution: + { + integrity: sha512-DI4PtTqzw9GwELFS41sdMK32Ajp3XZQ8iygeDMWkxlRhm7uUTOFSZFVZABFuxr0jvspn8MAYy54NxZCsuCTSOw==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + jest-runner@30.1.3: + resolution: + { + integrity: sha512-dd1ORcxQraW44Uz029TtXj85W11yvLpDuIzNOlofrC8GN+SgDlgY4BvyxJiVeuabA1t6idjNbX59jLd2oplOGQ==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + jest-runtime@30.1.3: + resolution: + { + integrity: sha512-WS8xgjuNSphdIGnleQcJ3AKE4tBKOVP+tKhCD0u+Tb2sBmsU8DxfbBpZX7//+XOz81zVs4eFpJQwBNji2Y07DA==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + jest-snapshot@30.1.2: + resolution: + { + integrity: sha512-4q4+6+1c8B6Cy5pGgFvjDy/Pa6VYRiGu0yQafKkJ9u6wQx4G5PqI2QR6nxTl43yy7IWsINwz6oT4o6tD12a8Dg==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + jest-util@29.7.0: resolution: { @@ -4891,6 +5795,27 @@ packages: } engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + jest-util@30.0.5: + resolution: + { + integrity: sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + jest-validate@30.1.0: + resolution: + { + integrity: sha512-7P3ZlCFW/vhfQ8pE7zW6Oi4EzvuB4sgR72Q1INfW9m0FGo0GADYlPwIkf4CyPq7wq85g+kPMtPOHNAdWHeBOaA==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + jest-watcher@30.1.3: + resolution: + { + integrity: sha512-6jQUZCP1BTL2gvG9E4YF06Ytq4yMb4If6YoQGRR6PpjtqOXSP3sKe2kqwB6SQ+H9DezOfZaSLnmka1NtGm3fCQ==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + jest-worker@29.7.0: resolution: { @@ -4898,6 +5823,26 @@ packages: } engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + jest-worker@30.1.0: + resolution: + { + integrity: sha512-uvWcSjlwAAgIu133Tt77A05H7RIk3Ho8tZL50bQM2AkvLdluw9NG48lRCl3Dt+MOH719n/0nnb5YxUwcuJiKRA==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + + jest@30.1.3: + resolution: + { + integrity: sha512-Ry+p2+NLk6u8Agh5yVqELfUJvRfV51hhVBRIB5yZPY7mU0DGBmOuFG5GebZbMbm86cdQNK0fhJuDX8/1YorISQ==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + jiti@1.21.7: resolution: { @@ -4911,12 +5856,26 @@ packages: integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==, } + js-levenshtein@1.1.6: + resolution: + { + integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==, + } + engines: { node: ">=0.10.0" } + js-tokens@4.0.0: resolution: { integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, } + js-yaml@3.14.1: + resolution: + { + integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==, + } + hasBin: true + js-yaml@4.1.0: resolution: { @@ -4981,6 +5940,14 @@ packages: } hasBin: true + json5@2.2.3: + resolution: + { + integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==, + } + engines: { node: ">=6" } + hasBin: true + jsonfile@4.0.0: resolution: { @@ -5019,6 +5986,13 @@ packages: } engines: { node: ">=0.10" } + leven@3.1.0: + resolution: + { + integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==, + } + engines: { node: ">=6" } + levn@0.4.1: resolution: { @@ -5057,6 +6031,13 @@ packages: integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==, } + locate-path@5.0.0: + resolution: + { + integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==, + } + engines: { node: ">=8" } + locate-path@6.0.0: resolution: { @@ -5076,6 +6057,12 @@ packages: integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==, } + lodash.memoize@4.1.2: + resolution: + { + integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==, + } + lodash.merge@4.6.2: resolution: { @@ -5101,6 +6088,12 @@ packages: integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==, } + lru-cache@5.1.1: + resolution: + { + integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==, + } + lru-cache@6.0.0: resolution: { @@ -5130,12 +6123,25 @@ packages: } engines: { node: ">=8" } + make-dir@4.0.0: + resolution: + { + integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==, + } + engines: { node: ">=10" } + make-error@1.3.6: resolution: { integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==, } + makeerror@1.0.12: + resolution: + { + integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==, + } + math-intrinsics@1.1.0: resolution: { @@ -5460,12 +6466,6 @@ packages: engines: { node: ">=10" } hasBin: true - mlly@1.7.4: - resolution: - { - integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==, - } - mri@1.2.0: resolution: { @@ -5566,24 +6566,12 @@ packages: sass: optional: true - node-abort-controller@3.1.1: - resolution: - { - integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==, - } - node-addon-api@7.1.1: resolution: { integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==, } - node-fetch-native@1.6.7: - resolution: - { - integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==, - } - node-fetch@2.6.7: resolution: { @@ -5627,6 +6615,12 @@ packages: } hasBin: true + node-int64@0.4.0: + resolution: + { + integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==, + } + node-releases@2.0.19: resolution: { @@ -5690,14 +6684,6 @@ packages: react-router-dom: optional: true - nypm@0.5.4: - resolution: - { - integrity: sha512-X0SNNrZiGU8/e/zAB7sCTtdxWTMSIO73q+xuKgglm2Yvzwlo8UoC5FNySQFCvl84uPaeADkqHUZUkWy4aH4xOA==, - } - engines: { node: ^14.16.0 || >=16.10.0 } - hasBin: true - oauth@0.9.15: resolution: { @@ -5774,12 +6760,6 @@ packages: } engines: { node: ">= 0.4" } - ohash@1.1.6: - resolution: - { - integrity: sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg==, - } - oidc-token-hash@5.1.1: resolution: { @@ -5806,6 +6786,36 @@ packages: } engines: { node: ">=6" } + openapi-fetch@0.14.0: + resolution: + { + integrity: sha512-PshIdm1NgdLvb05zp8LqRQMNSKzIlPkyMxYFxwyHR+UlKD4t2nUjkDhNxeRbhRSEd3x5EUNh2w5sJYwkhOH4fg==, + } + + openapi-react-query@0.5.0: + resolution: + { + integrity: sha512-VtyqiamsbWsdSWtXmj/fAR+m9nNxztsof6h8ZIsjRj8c8UR/x9AIwHwd60IqwgymmFwo7qfSJQ1ZzMJrtqjQVg==, + } + peerDependencies: + "@tanstack/react-query": ^5.25.0 + openapi-fetch: ^0.14.0 + + openapi-typescript-helpers@0.0.15: + resolution: + { + integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==, + } + + openapi-typescript@7.9.1: + resolution: + { + integrity: sha512-9gJtoY04mk6iPMbToPjPxEAtfXZ0dTsMZtsgUI8YZta0btPPig9DJFP4jlerQD/7QOwYgb0tl+zLUpDf7vb7VA==, + } + hasBin: true + peerDependencies: + typescript: ^5.x + openid-client@5.7.1: resolution: { @@ -5840,6 +6850,13 @@ packages: } engines: { node: ">=8" } + p-limit@2.3.0: + resolution: + { + integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==, + } + engines: { node: ">=6" } + p-limit@3.1.0: resolution: { @@ -5847,6 +6864,13 @@ packages: } engines: { node: ">=10" } + p-locate@4.1.0: + resolution: + { + integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==, + } + engines: { node: ">=8" } + p-locate@5.0.0: resolution: { @@ -5854,6 +6878,13 @@ packages: } engines: { node: ">=10" } + p-try@2.2.0: + resolution: + { + integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==, + } + engines: { node: ">=6" } + package-json-from-dist@1.0.1: resolution: { @@ -5880,6 +6911,13 @@ packages: } engines: { node: ">=8" } + parse-json@8.3.0: + resolution: + { + integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==, + } + engines: { node: ">=18" } + parse-ms@2.1.0: resolution: { @@ -5959,30 +6997,12 @@ packages: } engines: { node: ">=8" } - pathe@1.1.2: - resolution: - { - integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==, - } - - pathe@2.0.3: - resolution: - { - integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==, - } - pend@1.2.0: resolution: { integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==, } - perfect-debounce@1.0.0: - resolution: - { - integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==, - } - perfect-freehand@1.2.2: resolution: { @@ -6029,11 +7049,19 @@ packages: } engines: { node: ">= 6" } - pkg-types@1.3.1: + pkg-dir@4.2.0: resolution: { - integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==, + integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==, } + engines: { node: ">=8" } + + pluralize@8.0.0: + resolution: + { + integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==, + } + engines: { node: ">=4" } possible-typed-array-names@1.1.0: resolution: @@ -6146,6 +7174,13 @@ packages: integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==, } + pretty-format@30.0.5: + resolution: + { + integrity: sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==, + } + engines: { node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 } + pretty-ms@7.0.1: resolution: { @@ -6209,6 +7244,12 @@ packages: } engines: { node: ">=6" } + pure-rand@7.0.1: + resolution: + { + integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==, + } + qr.js@0.0.0: resolution: { @@ -6234,12 +7275,6 @@ packages: } engines: { node: ">= 0.8" } - rc9@2.1.2: - resolution: - { - integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==, - } - react-dom@18.3.1: resolution: { @@ -6271,6 +7306,12 @@ packages: integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==, } + react-is@18.3.1: + resolution: + { + integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==, + } + react-markdown@9.1.0: resolution: { @@ -6392,13 +7433,6 @@ packages: } engines: { node: ">=4" } - redlock@5.0.0-beta.2: - resolution: - { - integrity: sha512-2RDWXg5jgRptDrB1w9O/JgSZC0j7y4SlaXnor93H/UJm/QyDiFgBKNtrh0TI6oCXqYSaSoXxFh6Sd3VtYfhRXw==, - } - engines: { node: ">=12" } - redux-thunk@3.1.0: resolution: { @@ -6439,6 +7473,13 @@ packages: integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==, } + require-directory@2.1.1: + resolution: + { + integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==, + } + engines: { node: ">=0.10.0" } + require-from-string@2.0.2: resolution: { @@ -6458,6 +7499,13 @@ packages: integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==, } + resolve-cwd@3.0.0: + resolution: + { + integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==, + } + engines: { node: ">=8" } + resolve-from@4.0.0: resolution: { @@ -6727,6 +7775,13 @@ packages: integrity: sha512-D1SaWpOW8afq1CZGWB8xTfrT3FekjQmPValrqncJMX7QFl8YwhrPTZvMCANLtgBwwdS+7zURyqxDDEmY558tTw==, } + slash@3.0.0: + resolution: + { + integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==, + } + engines: { node: ">=8" } + socket.io-client@4.7.2: resolution: { @@ -6748,6 +7803,12 @@ packages: } engines: { node: ">=0.10.0" } + source-map-support@0.5.13: + resolution: + { + integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==, + } + source-map@0.5.7: resolution: { @@ -6768,6 +7829,12 @@ packages: integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==, } + sprintf-js@1.0.3: + resolution: + { + integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==, + } + sprintf-js@1.1.3: resolution: { @@ -6780,6 +7847,13 @@ packages: integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==, } + stack-utils@2.0.6: + resolution: + { + integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==, + } + engines: { node: ">=10" } + stacktrace-parser@0.1.11: resolution: { @@ -6832,6 +7906,13 @@ packages: } engines: { node: ">=10.0.0" } + string-length@4.0.2: + resolution: + { + integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==, + } + engines: { node: ">=10" } + string-width@4.2.3: resolution: { @@ -6920,6 +8001,13 @@ packages: } engines: { node: ">=4" } + strip-bom@4.0.0: + resolution: + { + integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==, + } + engines: { node: ">=8" } + strip-final-newline@2.0.0: resolution: { @@ -6976,6 +8064,13 @@ packages: engines: { node: ">=16 || 14 >=14.17" } hasBin: true + supports-color@10.2.0: + resolution: + { + integrity: sha512-5eG9FQjEjDbAlI5+kdpdyPIBMRH4GfTVDGREVupaZHmVoppknhM29b/S9BkQz7cathp85BVgRi/As3Siln7e0Q==, + } + engines: { node: ">=18" } + supports-color@7.2.0: resolution: { @@ -7004,6 +8099,13 @@ packages: } engines: { node: ">= 0.4" } + synckit@0.11.11: + resolution: + { + integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==, + } + engines: { node: ^14.18.0 || >=16.0.0 } + tailwindcss@3.4.17: resolution: { @@ -7026,6 +8128,13 @@ packages: } engines: { node: ">=10" } + test-exclude@6.0.0: + resolution: + { + integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==, + } + engines: { node: ">=8" } + thenify-all@1.6.0: resolution: { @@ -7046,12 +8155,6 @@ packages: } engines: { node: ">=10" } - tinyexec@0.3.2: - resolution: - { - integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==, - } - tinyglobby@0.2.14: resolution: { @@ -7059,6 +8162,12 @@ packages: } engines: { node: ">=12.0.0" } + tmpl@1.0.5: + resolution: + { + integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==, + } + to-regex-range@5.0.1: resolution: { @@ -7113,6 +8222,36 @@ packages: integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==, } + ts-jest@29.4.1: + resolution: + { + integrity: sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==, + } + engines: { node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0 } + hasBin: true + peerDependencies: + "@babel/core": ">=7.0.0-beta.0 <8" + "@jest/transform": ^29.0.0 || ^30.0.0 + "@jest/types": ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 + esbuild: "*" + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 + typescript: ">=4.3 <6" + peerDependenciesMeta: + "@babel/core": + optional: true + "@jest/transform": + optional: true + "@jest/types": + optional: true + babel-jest: + optional: true + esbuild: + optional: true + jest-util: + optional: true + ts-morph@12.0.0: resolution: { @@ -7161,6 +8300,20 @@ packages: } engines: { node: ">= 0.8.0" } + type-detect@4.0.8: + resolution: + { + integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==, + } + engines: { node: ">=4" } + + type-fest@0.21.3: + resolution: + { + integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==, + } + engines: { node: ">=10" } + type-fest@0.7.1: resolution: { @@ -7168,6 +8321,13 @@ packages: } engines: { node: ">=8" } + type-fest@4.41.0: + resolution: + { + integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==, + } + engines: { node: ">=16" } + typed-array-buffer@1.0.3: resolution: { @@ -7244,12 +8404,6 @@ packages: integrity: sha512-v+Z8Jal+GtmKGtJ34GIQlCJAxrDt9kbjpNsNvYoAXFyr4gNfWlD4uJJuoNNu/0UTVaKvQwHaSU095YDl71lKPw==, } - ufo@1.6.1: - resolution: - { - integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==, - } - uglify-js@3.19.3: resolution: { @@ -7289,12 +8443,6 @@ packages: } engines: { node: ">= 0.4" } - uncrypto@0.1.3: - resolution: - { - integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==, - } - undici-types@7.10.0: resolution: { @@ -7386,6 +8534,12 @@ packages: integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==, } + uri-js-replace@1.0.1: + resolution: + { + integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==, + } + uri-js@4.4.1: resolution: { @@ -7464,6 +8618,13 @@ packages: integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==, } + v8-to-istanbul@9.3.0: + resolution: + { + integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==, + } + engines: { node: ">=10.12.0" } + vercel@37.14.0: resolution: { @@ -7484,6 +8645,12 @@ packages: integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==, } + walker@1.0.8: + resolution: + { + integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==, + } + wavesurfer.js@7.10.1: resolution: { @@ -7597,6 +8764,13 @@ packages: integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==, } + write-file-atomic@5.0.1: + resolution: + { + integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==, + } + engines: { node: ^14.17.0 || ^16.13.0 || >=18.0.0 } + ws@8.17.1: resolution: { @@ -7633,6 +8807,13 @@ packages: } engines: { node: ">=0.4.0" } + y18n@5.0.8: + resolution: + { + integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==, + } + engines: { node: ">=10" } + yallist@3.1.1: resolution: { @@ -7645,6 +8826,12 @@ packages: integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==, } + yaml-ast-parser@0.0.43: + resolution: + { + integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==, + } + yaml@1.10.2: resolution: { @@ -7660,6 +8847,20 @@ packages: engines: { node: ">= 14.6" } hasBin: true + yargs-parser@21.1.1: + resolution: + { + integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==, + } + engines: { node: ">=12" } + + yargs@17.7.2: + resolution: + { + integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==, + } + engines: { node: ">=12" } + yauzl-clone@1.0.4: resolution: { @@ -7694,6 +8895,12 @@ packages: } engines: { node: ">=10" } + zod@4.1.5: + resolution: + { + integrity: sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==, + } + zwitch@2.0.4: resolution: { @@ -7703,11 +8910,10 @@ packages: snapshots: "@alloc/quick-lru@5.2.0": {} - "@apidevtools/json-schema-ref-parser@11.6.4": + "@ampproject/remapping@2.3.0": dependencies: - "@jsdevtools/ono": 7.1.3 - "@types/json-schema": 7.0.15 - js-yaml: 4.1.0 + "@jridgewell/gen-mapping": 0.3.13 + "@jridgewell/trace-mapping": 0.3.30 "@ark-ui/react@5.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": dependencies: @@ -7779,6 +8985,28 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + "@babel/compat-data@7.28.0": {} + + "@babel/core@7.28.3": + dependencies: + "@ampproject/remapping": 2.3.0 + "@babel/code-frame": 7.27.1 + "@babel/generator": 7.28.3 + "@babel/helper-compilation-targets": 7.27.2 + "@babel/helper-module-transforms": 7.28.3(@babel/core@7.28.3) + "@babel/helpers": 7.28.3 + "@babel/parser": 7.28.3 + "@babel/template": 7.27.2 + "@babel/traverse": 7.28.3 + "@babel/types": 7.28.2 + convert-source-map: 2.0.0 + debug: 4.4.1(supports-color@9.4.0) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + "@babel/generator@7.28.0": dependencies: "@babel/parser": 7.28.0 @@ -7787,6 +9015,22 @@ snapshots: "@jridgewell/trace-mapping": 0.3.30 jsesc: 3.1.0 + "@babel/generator@7.28.3": + dependencies: + "@babel/parser": 7.28.3 + "@babel/types": 7.28.2 + "@jridgewell/gen-mapping": 0.3.13 + "@jridgewell/trace-mapping": 0.3.30 + jsesc: 3.1.0 + + "@babel/helper-compilation-targets@7.27.2": + dependencies: + "@babel/compat-data": 7.28.0 + "@babel/helper-validator-option": 7.27.1 + browserslist: 4.25.2 + lru-cache: 5.1.1 + semver: 6.3.1 + "@babel/helper-globals@7.28.0": {} "@babel/helper-module-imports@7.27.1": @@ -7796,14 +9040,121 @@ snapshots: transitivePeerDependencies: - supports-color + "@babel/helper-module-transforms@7.28.3(@babel/core@7.28.3)": + dependencies: + "@babel/core": 7.28.3 + "@babel/helper-module-imports": 7.27.1 + "@babel/helper-validator-identifier": 7.27.1 + "@babel/traverse": 7.28.3 + transitivePeerDependencies: + - supports-color + + "@babel/helper-plugin-utils@7.27.1": {} + "@babel/helper-string-parser@7.27.1": {} "@babel/helper-validator-identifier@7.27.1": {} + "@babel/helper-validator-option@7.27.1": {} + + "@babel/helpers@7.28.3": + dependencies: + "@babel/template": 7.27.2 + "@babel/types": 7.28.2 + "@babel/parser@7.28.0": dependencies: "@babel/types": 7.28.2 + "@babel/parser@7.28.3": + dependencies: + "@babel/types": 7.28.2 + + "@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.3)": + dependencies: + "@babel/core": 7.28.3 + "@babel/helper-plugin-utils": 7.27.1 + + "@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.3)": + dependencies: + "@babel/core": 7.28.3 + "@babel/helper-plugin-utils": 7.27.1 + + "@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.3)": + dependencies: + "@babel/core": 7.28.3 + "@babel/helper-plugin-utils": 7.27.1 + + "@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.3)": + dependencies: + "@babel/core": 7.28.3 + "@babel/helper-plugin-utils": 7.27.1 + + "@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.3)": + dependencies: + "@babel/core": 7.28.3 + "@babel/helper-plugin-utils": 7.27.1 + + "@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.3)": + dependencies: + "@babel/core": 7.28.3 + "@babel/helper-plugin-utils": 7.27.1 + + "@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.3)": + dependencies: + "@babel/core": 7.28.3 + "@babel/helper-plugin-utils": 7.27.1 + + "@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.3)": + dependencies: + "@babel/core": 7.28.3 + "@babel/helper-plugin-utils": 7.27.1 + + "@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.3)": + dependencies: + "@babel/core": 7.28.3 + "@babel/helper-plugin-utils": 7.27.1 + + "@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.3)": + dependencies: + "@babel/core": 7.28.3 + "@babel/helper-plugin-utils": 7.27.1 + + "@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.3)": + dependencies: + "@babel/core": 7.28.3 + "@babel/helper-plugin-utils": 7.27.1 + + "@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.3)": + dependencies: + "@babel/core": 7.28.3 + "@babel/helper-plugin-utils": 7.27.1 + + "@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.3)": + dependencies: + "@babel/core": 7.28.3 + "@babel/helper-plugin-utils": 7.27.1 + + "@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.3)": + dependencies: + "@babel/core": 7.28.3 + "@babel/helper-plugin-utils": 7.27.1 + + "@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.3)": + dependencies: + "@babel/core": 7.28.3 + "@babel/helper-plugin-utils": 7.27.1 + + "@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.3)": + dependencies: + "@babel/core": 7.28.3 + "@babel/helper-plugin-utils": 7.27.1 + + "@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.3)": + dependencies: + "@babel/core": 7.28.3 + "@babel/helper-plugin-utils": 7.27.1 + "@babel/runtime@7.28.2": {} "@babel/template@7.27.2": @@ -7824,11 +9175,25 @@ snapshots: transitivePeerDependencies: - supports-color + "@babel/traverse@7.28.3": + dependencies: + "@babel/code-frame": 7.27.1 + "@babel/generator": 7.28.3 + "@babel/helper-globals": 7.28.0 + "@babel/parser": 7.28.3 + "@babel/template": 7.27.2 + "@babel/types": 7.28.2 + debug: 4.4.1(supports-color@9.4.0) + transitivePeerDependencies: + - supports-color + "@babel/types@7.28.2": dependencies: "@babel/helper-string-parser": 7.27.1 "@babel/helper-validator-identifier": 7.27.1 + "@bcoe/v8-coverage@0.2.3": {} + "@chakra-ui/react@3.24.2(@emotion/react@11.14.0(@types/react@18.2.20)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": dependencies: "@ark-ui/react": 5.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -8027,17 +9392,6 @@ snapshots: prop-types: 15.8.1 react: 18.3.1 - "@hey-api/openapi-ts@0.48.3(typescript@5.9.2)": - dependencies: - "@apidevtools/json-schema-ref-parser": 11.6.4 - c12: 1.11.1 - camelcase: 8.0.0 - commander: 12.1.0 - handlebars: 4.7.8 - typescript: 5.9.2 - transitivePeerDependencies: - - magicast - "@humanfs/core@0.19.1": {} "@humanfs/node@0.16.6": @@ -8059,7 +9413,7 @@ snapshots: dependencies: "@swc/helpers": 0.5.17 - "@ioredis/commands@1.3.0": {} + "@ioredis/commands@1.3.1": {} "@isaacs/cliui@8.0.2": dependencies: @@ -8070,10 +9424,189 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + "@istanbuljs/load-nyc-config@1.1.0": + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + + "@istanbuljs/schema@0.1.3": {} + + "@jest/console@30.1.2": + dependencies: + "@jest/types": 30.0.5 + "@types/node": 24.2.1 + chalk: 4.1.2 + jest-message-util: 30.1.0 + jest-util: 30.0.5 + slash: 3.0.0 + + "@jest/core@30.1.3(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@types/node@16.18.11)(typescript@5.9.2))": + dependencies: + "@jest/console": 30.1.2 + "@jest/pattern": 30.0.1 + "@jest/reporters": 30.1.3 + "@jest/test-result": 30.1.3 + "@jest/transform": 30.1.2 + "@jest/types": 30.0.5 + "@types/node": 24.2.1 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 4.3.0 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-changed-files: 30.0.5 + jest-config: 30.1.3(@types/node@24.2.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@types/node@16.18.11)(typescript@5.9.2)) + jest-haste-map: 30.1.0 + jest-message-util: 30.1.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.1.3 + jest-resolve-dependencies: 30.1.3 + jest-runner: 30.1.3 + jest-runtime: 30.1.3 + jest-snapshot: 30.1.2 + jest-util: 30.0.5 + jest-validate: 30.1.0 + jest-watcher: 30.1.3 + micromatch: 4.0.8 + pretty-format: 30.0.5 + slash: 3.0.0 + transitivePeerDependencies: + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + "@jest/diff-sequences@30.0.1": {} + + "@jest/environment@30.1.2": + dependencies: + "@jest/fake-timers": 30.1.2 + "@jest/types": 30.0.5 + "@types/node": 24.2.1 + jest-mock: 30.0.5 + + "@jest/expect-utils@30.1.2": + dependencies: + "@jest/get-type": 30.1.0 + + "@jest/expect@30.1.2": + dependencies: + expect: 30.1.2 + jest-snapshot: 30.1.2 + transitivePeerDependencies: + - supports-color + + "@jest/fake-timers@30.1.2": + dependencies: + "@jest/types": 30.0.5 + "@sinonjs/fake-timers": 13.0.5 + "@types/node": 24.2.1 + jest-message-util: 30.1.0 + jest-mock: 30.0.5 + jest-util: 30.0.5 + + "@jest/get-type@30.1.0": {} + + "@jest/globals@30.1.2": + dependencies: + "@jest/environment": 30.1.2 + "@jest/expect": 30.1.2 + "@jest/types": 30.0.5 + jest-mock: 30.0.5 + transitivePeerDependencies: + - supports-color + + "@jest/pattern@30.0.1": + dependencies: + "@types/node": 24.2.1 + jest-regex-util: 30.0.1 + + "@jest/reporters@30.1.3": + dependencies: + "@bcoe/v8-coverage": 0.2.3 + "@jest/console": 30.1.2 + "@jest/test-result": 30.1.3 + "@jest/transform": 30.1.2 + "@jest/types": 30.0.5 + "@jridgewell/trace-mapping": 0.3.30 + "@types/node": 24.2.1 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit-x: 0.2.2 + glob: 10.4.5 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + jest-message-util: 30.1.0 + jest-util: 30.0.5 + jest-worker: 30.1.0 + slash: 3.0.0 + string-length: 4.0.2 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + "@jest/schemas@29.6.3": dependencies: "@sinclair/typebox": 0.27.8 + "@jest/schemas@30.0.5": + dependencies: + "@sinclair/typebox": 0.34.41 + + "@jest/snapshot-utils@30.1.2": + dependencies: + "@jest/types": 30.0.5 + chalk: 4.1.2 + graceful-fs: 4.2.11 + natural-compare: 1.4.0 + + "@jest/source-map@30.0.1": + dependencies: + "@jridgewell/trace-mapping": 0.3.30 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + "@jest/test-result@30.1.3": + dependencies: + "@jest/console": 30.1.2 + "@jest/types": 30.0.5 + "@types/istanbul-lib-coverage": 2.0.6 + collect-v8-coverage: 1.0.2 + + "@jest/test-sequencer@30.1.3": + dependencies: + "@jest/test-result": 30.1.3 + graceful-fs: 4.2.11 + jest-haste-map: 30.1.0 + slash: 3.0.0 + + "@jest/transform@30.1.2": + dependencies: + "@babel/core": 7.28.3 + "@jest/types": 30.0.5 + "@jridgewell/trace-mapping": 0.3.30 + babel-plugin-istanbul: 7.0.0 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.1.0 + jest-regex-util: 30.0.1 + jest-util: 30.0.5 + micromatch: 4.0.8 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + "@jest/types@29.6.3": dependencies: "@jest/schemas": 29.6.3 @@ -8083,6 +9616,16 @@ snapshots: "@types/yargs": 17.0.33 chalk: 4.1.2 + "@jest/types@30.0.5": + dependencies: + "@jest/pattern": 30.0.1 + "@jest/schemas": 30.0.5 + "@types/istanbul-lib-coverage": 2.0.6 + "@types/istanbul-reports": 3.0.4 + "@types/node": 24.2.1 + "@types/yargs": 17.0.33 + chalk: 4.1.2 + "@jridgewell/gen-mapping@0.3.13": dependencies: "@jridgewell/sourcemap-codec": 1.5.5 @@ -8102,8 +9645,6 @@ snapshots: "@jridgewell/resolve-uri": 3.1.2 "@jridgewell/sourcemap-codec": 1.5.5 - "@jsdevtools/ono@7.1.3": {} - "@mapbox/node-pre-gyp@1.0.11": dependencies: detect-libc: 2.0.4 @@ -8241,6 +9782,8 @@ snapshots: "@pkgjs/parseargs@0.11.0": optional: true + "@pkgr/core@0.2.9": {} + "@radix-ui/primitive@1.1.3": {} "@radix-ui/react-arrow@1.1.7(@types/react@18.2.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)": @@ -8420,6 +9963,29 @@ snapshots: "@radix-ui/rect@1.1.1": {} + "@redocly/ajv@8.11.3": + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js-replace: 1.0.1 + + "@redocly/config@0.22.2": {} + + "@redocly/openapi-core@1.34.5(supports-color@10.2.0)": + dependencies: + "@redocly/ajv": 8.11.3 + "@redocly/config": 0.22.2 + colorette: 1.4.0 + https-proxy-agent: 7.0.6(supports-color@10.2.0) + js-levenshtein: 1.1.6 + js-yaml: 4.1.0 + minimatch: 5.1.6 + pluralize: 8.0.0 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - supports-color + "@reduxjs/toolkit@2.8.2(react@18.3.1)": dependencies: "@standard-schema/spec": 1.0.0 @@ -8513,7 +10079,7 @@ snapshots: "@sentry/utils": 7.120.4 localforage: 1.10.0 - "@sentry/nextjs@7.120.4(next@14.2.31(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(react@18.3.1)": + "@sentry/nextjs@7.120.4(next@14.2.31(@babel/core@7.28.3)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(react@18.3.1)": dependencies: "@rollup/plugin-commonjs": 24.0.0(rollup@2.79.2) "@sentry/core": 7.120.4 @@ -8525,7 +10091,7 @@ snapshots: "@sentry/vercel-edge": 7.120.4 "@sentry/webpack-plugin": 1.21.0 chalk: 3.0.0 - next: 14.2.31(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0) + next: 14.2.31(@babel/core@7.28.3)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0) react: 18.3.1 resolve: 1.22.8 rollup: 2.79.2 @@ -8584,6 +10150,16 @@ snapshots: "@sinclair/typebox@0.27.8": {} + "@sinclair/typebox@0.34.41": {} + + "@sinonjs/commons@3.0.1": + dependencies: + type-detect: 4.0.8 + + "@sinonjs/fake-timers@13.0.5": + dependencies: + "@sinonjs/commons": 3.0.1 + "@socket.io/component-emitter@3.1.2": {} "@standard-schema/spec@1.0.0": {} @@ -8601,6 +10177,13 @@ snapshots: "@swc/counter": 0.1.3 tslib: 2.8.1 + "@tanstack/query-core@5.85.9": {} + + "@tanstack/react-query@5.85.9(react@18.3.1)": + dependencies: + "@tanstack/query-core": 5.85.9 + react: 18.3.1 + "@tootallnate/once@2.0.0": {} "@ts-morph/common@0.11.1": @@ -8623,6 +10206,27 @@ snapshots: tslib: 2.8.1 optional: true + "@types/babel__core@7.20.5": + dependencies: + "@babel/parser": 7.28.0 + "@babel/types": 7.28.2 + "@types/babel__generator": 7.27.0 + "@types/babel__template": 7.4.4 + "@types/babel__traverse": 7.28.0 + + "@types/babel__generator@7.27.0": + dependencies: + "@babel/types": 7.28.2 + + "@types/babel__template@7.4.4": + dependencies: + "@babel/parser": 7.28.0 + "@babel/types": 7.28.2 + + "@types/babel__traverse@7.28.0": + dependencies: + "@babel/types": 7.28.2 + "@types/debug@4.1.12": dependencies: "@types/ms": 2.1.0 @@ -8639,6 +10243,12 @@ snapshots: dependencies: "@types/unist": 3.0.3 + "@types/ioredis@5.0.0": + dependencies: + ioredis: 5.7.0 + transitivePeerDependencies: + - supports-color + "@types/istanbul-lib-coverage@2.0.6": {} "@types/istanbul-lib-report@3.0.3": @@ -8649,6 +10259,11 @@ snapshots: dependencies: "@types/istanbul-lib-report": 3.0.3 + "@types/jest@30.0.0": + dependencies: + expect: 30.1.2 + pretty-format: 30.0.5 + "@types/json-schema@7.0.15": {} "@types/json5@0.0.29": {} @@ -8682,6 +10297,8 @@ snapshots: "@types/scheduler@0.26.0": {} + "@types/stack-utils@2.0.3": {} + "@types/ua-parser-js@0.7.39": {} "@types/unist@2.0.11": {} @@ -8860,10 +10477,6 @@ snapshots: "@unrs/resolver-binding-win32-x64-msvc@1.11.1": optional: true - "@upstash/redis@1.35.3": - dependencies: - uncrypto: 0.1.3 - "@vercel/build-utils@8.4.12": {} "@vercel/edge-config-fs@0.1.0": {} @@ -8920,10 +10533,6 @@ snapshots: "@vercel/static-config": 3.0.0 ts-morph: 12.0.0 - "@vercel/kv@2.0.0": - dependencies: - "@upstash/redis": 1.35.3 - "@vercel/next@4.3.18": dependencies: "@vercel/nft": 0.27.3 @@ -9601,6 +11210,8 @@ snapshots: transitivePeerDependencies: - supports-color + agent-base@7.1.4: {} + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -9615,6 +11226,12 @@ snapshots: require-from-string: 2.0.2 uri-js: 4.4.1 + ansi-colors@4.1.3: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + ansi-regex@5.0.1: {} ansi-regex@6.1.0: {} @@ -9623,6 +11240,8 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@5.2.0: {} + ansi-styles@6.2.1: {} any-promise@1.3.0: {} @@ -9645,6 +11264,10 @@ snapshots: arg@5.0.2: {} + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + argparse@2.0.1: {} aria-hidden@1.2.6: @@ -9771,12 +11394,66 @@ snapshots: axobject-query@4.1.0: {} + babel-jest@30.1.2(@babel/core@7.28.3): + dependencies: + "@babel/core": 7.28.3 + "@jest/transform": 30.1.2 + "@types/babel__core": 7.20.5 + babel-plugin-istanbul: 7.0.0 + babel-preset-jest: 30.0.1(@babel/core@7.28.3) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@7.0.0: + dependencies: + "@babel/helper-plugin-utils": 7.27.1 + "@istanbuljs/load-nyc-config": 1.1.0 + "@istanbuljs/schema": 0.1.3 + istanbul-lib-instrument: 6.0.3 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@30.0.1: + dependencies: + "@babel/template": 7.27.2 + "@babel/types": 7.28.2 + "@types/babel__core": 7.20.5 + babel-plugin-macros@3.1.0: dependencies: "@babel/runtime": 7.28.2 cosmiconfig: 7.1.0 resolve: 1.22.10 + babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.3): + dependencies: + "@babel/core": 7.28.3 + "@babel/plugin-syntax-async-generators": 7.8.4(@babel/core@7.28.3) + "@babel/plugin-syntax-bigint": 7.8.3(@babel/core@7.28.3) + "@babel/plugin-syntax-class-properties": 7.12.13(@babel/core@7.28.3) + "@babel/plugin-syntax-class-static-block": 7.14.5(@babel/core@7.28.3) + "@babel/plugin-syntax-import-attributes": 7.27.1(@babel/core@7.28.3) + "@babel/plugin-syntax-import-meta": 7.10.4(@babel/core@7.28.3) + "@babel/plugin-syntax-json-strings": 7.8.3(@babel/core@7.28.3) + "@babel/plugin-syntax-logical-assignment-operators": 7.10.4(@babel/core@7.28.3) + "@babel/plugin-syntax-nullish-coalescing-operator": 7.8.3(@babel/core@7.28.3) + "@babel/plugin-syntax-numeric-separator": 7.10.4(@babel/core@7.28.3) + "@babel/plugin-syntax-object-rest-spread": 7.8.3(@babel/core@7.28.3) + "@babel/plugin-syntax-optional-catch-binding": 7.8.3(@babel/core@7.28.3) + "@babel/plugin-syntax-optional-chaining": 7.8.3(@babel/core@7.28.3) + "@babel/plugin-syntax-private-property-in-object": 7.14.5(@babel/core@7.28.3) + "@babel/plugin-syntax-top-level-await": 7.14.5(@babel/core@7.28.3) + + babel-preset-jest@30.0.1(@babel/core@7.28.3): + dependencies: + "@babel/core": 7.28.3 + babel-plugin-jest-hoist: 30.0.1 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.3) + bail@2.0.2: {} balanced-match@1.0.2: {} @@ -9809,10 +11486,20 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.2) + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + btoa@1.2.1: {} buffer-crc32@0.2.13: {} + buffer-from@1.1.2: {} + buffer@6.0.3: dependencies: base64-js: 1.5.1 @@ -9824,21 +11511,6 @@ snapshots: bytes@3.1.0: {} - c12@1.11.1: - dependencies: - chokidar: 3.6.0 - confbox: 0.1.8 - defu: 6.1.4 - dotenv: 16.6.1 - giget: 1.2.5 - jiti: 1.21.7 - mlly: 1.7.4 - ohash: 1.1.6 - pathe: 1.1.2 - perfect-debounce: 1.0.0 - pkg-types: 1.3.1 - rc9: 2.1.2 - call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -9860,7 +11532,9 @@ snapshots: camelcase-css@2.0.1: {} - camelcase@8.0.0: {} + camelcase@5.3.1: {} + + camelcase@6.3.0: {} caniuse-lite@1.0.30001734: {} @@ -9876,6 +11550,10 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + change-case@5.4.4: {} + + char-regex@1.0.2: {} + character-entities-html4@2.1.0: {} character-entities-legacy@3.0.0: {} @@ -9922,22 +11600,32 @@ snapshots: ci-info@3.9.0: {} - citty@0.1.6: - dependencies: - consola: 3.4.2 + ci-info@4.3.0: {} cjs-module-lexer@1.2.3: {} + cjs-module-lexer@2.1.0: {} + classnames@2.5.1: {} client-only@0.0.1: {} + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + clsx@2.1.1: {} cluster-key-slot@1.1.2: {} + co@4.6.0: {} + code-block-writer@10.1.1: {} + collect-v8-coverage@1.0.2: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -9946,24 +11634,20 @@ snapshots: color-support@1.1.3: {} + colorette@1.4.0: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 comma-separated-tokens@2.0.3: {} - commander@12.1.0: {} - commander@4.1.1: {} commondir@1.0.1: {} concat-map@0.0.1: {} - confbox@0.1.8: {} - - consola@3.4.2: {} - console-control-strings@1.1.0: {} content-type@1.0.4: {} @@ -9972,6 +11656,8 @@ snapshots: convert-source-map@1.9.0: {} + convert-source-map@2.0.0: {} + cookie@0.7.2: {} cosmiconfig@7.1.0: @@ -10026,6 +11712,12 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.1(supports-color@10.2.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 10.2.0 + debug@4.4.1(supports-color@9.4.0): dependencies: ms: 2.1.3 @@ -10036,8 +11728,14 @@ snapshots: dependencies: character-entities: 2.0.2 + dedent@1.7.0(babel-plugin-macros@3.1.0): + optionalDependencies: + babel-plugin-macros: 3.1.0 + deep-is@0.1.4: {} + deepmerge@4.3.1: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -10050,8 +11748,6 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 - defu@6.1.4: {} - delayed-stream@1.0.0: {} delegates@1.0.0: {} @@ -10062,8 +11758,6 @@ snapshots: dequal@2.0.3: {} - destr@2.0.5: {} - detect-europe-js@0.1.2: {} detect-libc@1.0.3: @@ -10071,6 +11765,8 @@ snapshots: detect-libc@2.0.4: {} + detect-newline@3.1.0: {} + detect-node-es@1.1.0: {} devlop@1.1.0: @@ -10103,8 +11799,6 @@ snapshots: domsanitizer: 0.2.3 umap: 1.0.2 - dotenv@16.6.1: {} - dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -10127,6 +11821,8 @@ snapshots: electron-to-chromium@1.5.200: {} + emittery@0.13.1: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -10347,6 +12043,8 @@ snapshots: escalade@3.2.0: {} + escape-string-regexp@2.0.0: {} + escape-string-regexp@4.0.0: {} eslint-config-next@14.2.31(eslint@9.33.0(jiti@1.21.7))(typescript@5.9.2): @@ -10534,6 +12232,8 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.15.0) eslint-visitor-keys: 4.2.1 + esprima@4.0.1: {} + esquery@1.6.0: dependencies: estraverse: 5.3.0 @@ -10571,6 +12271,29 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + exit-x@0.2.2: {} + + expect@30.1.2: + dependencies: + "@jest/expect-utils": 30.1.2 + "@jest/get-type": 30.1.0 + jest-matcher-utils: 30.1.2 + jest-message-util: 30.1.0 + jest-mock: 30.0.5 + jest-util: 30.0.5 + extend@3.0.2: {} fake-mediastreamtrack@1.2.0: @@ -10598,6 +12321,10 @@ snapshots: dependencies: reusify: 1.1.0 + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + fd-slicer@1.1.0: dependencies: pend: 1.2.0 @@ -10618,6 +12345,11 @@ snapshots: find-root@1.1.0: {} + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -10708,8 +12440,12 @@ snapshots: generic-pool@3.4.2: {} + gensync@1.0.0-beta.2: {} + get-browser-rtc@1.1.0: {} + get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -10725,6 +12461,8 @@ snapshots: get-nonce@1.0.1: {} + get-package-type@0.1.0: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -10734,6 +12472,8 @@ snapshots: dependencies: pump: 3.0.3 + get-stream@6.0.1: {} + get-symbol-description@1.1.0: dependencies: call-bound: 1.0.4 @@ -10744,16 +12484,6 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - giget@1.2.5: - dependencies: - citty: 0.1.6 - consola: 3.4.2 - defu: 6.1.4 - node-fetch-native: 1.6.7 - nypm: 0.5.4 - pathe: 2.0.3 - tar: 6.2.1 - glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -10885,6 +12615,8 @@ snapshots: dependencies: react-is: 16.13.1 + html-escaper@2.0.2: {} + html-url-attributes@3.0.1: {} http-errors@1.4.0: @@ -10907,8 +12639,17 @@ snapshots: transitivePeerDependencies: - supports-color + https-proxy-agent@7.0.6(supports-color@10.2.0): + dependencies: + agent-base: 7.1.4 + debug: 4.4.1(supports-color@10.2.0) + transitivePeerDependencies: + - supports-color + human-signals@1.1.1: {} + human-signals@2.1.0: {} + hyperhtml-style@0.1.3: {} iconv-lite@0.4.24: @@ -10932,8 +12673,15 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + imurmurhash@0.1.4: {} + index-to-position@1.1.0: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -10953,7 +12701,7 @@ snapshots: ioredis@5.7.0: dependencies: - "@ioredis/commands": 1.3.0 + "@ioredis/commands": 1.3.1 cluster-key-slot: 1.1.2 debug: 4.4.1(supports-color@9.4.0) denque: 2.1.0 @@ -11043,6 +12791,8 @@ snapshots: is-fullwidth-code-point@3.0.0: {} + is-generator-fn@2.1.0: {} + is-generator-function@1.1.0: dependencies: call-bound: 1.0.4 @@ -11122,6 +12872,37 @@ snapshots: isexe@2.0.0: {} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@6.0.3: + dependencies: + "@babel/core": 7.28.3 + "@babel/parser": 7.28.0 + "@istanbuljs/schema": 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + "@jridgewell/trace-mapping": 0.3.30 + debug: 4.4.1(supports-color@9.4.0) + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + iterator.prototype@1.1.5: dependencies: define-data-property: 1.1.4 @@ -11143,6 +12924,301 @@ snapshots: optionalDependencies: "@pkgjs/parseargs": 0.11.0 + jest-changed-files@30.0.5: + dependencies: + execa: 5.1.1 + jest-util: 30.0.5 + p-limit: 3.1.0 + + jest-circus@30.1.3(babel-plugin-macros@3.1.0): + dependencies: + "@jest/environment": 30.1.2 + "@jest/expect": 30.1.2 + "@jest/test-result": 30.1.3 + "@jest/types": 30.0.5 + "@types/node": 24.2.1 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.7.0(babel-plugin-macros@3.1.0) + is-generator-fn: 2.1.0 + jest-each: 30.1.0 + jest-matcher-utils: 30.1.2 + jest-message-util: 30.1.0 + jest-runtime: 30.1.3 + jest-snapshot: 30.1.2 + jest-util: 30.0.5 + p-limit: 3.1.0 + pretty-format: 30.0.5 + pure-rand: 7.0.1 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@30.1.3(@types/node@16.18.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@types/node@16.18.11)(typescript@5.9.2)): + dependencies: + "@jest/core": 30.1.3(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@types/node@16.18.11)(typescript@5.9.2)) + "@jest/test-result": 30.1.3 + "@jest/types": 30.0.5 + chalk: 4.1.2 + exit-x: 0.2.2 + import-local: 3.2.0 + jest-config: 30.1.3(@types/node@16.18.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@types/node@16.18.11)(typescript@5.9.2)) + jest-util: 30.0.5 + jest-validate: 30.1.0 + yargs: 17.7.2 + transitivePeerDependencies: + - "@types/node" + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + jest-config@30.1.3(@types/node@16.18.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@types/node@16.18.11)(typescript@5.9.2)): + dependencies: + "@babel/core": 7.28.3 + "@jest/get-type": 30.1.0 + "@jest/pattern": 30.0.1 + "@jest/test-sequencer": 30.1.3 + "@jest/types": 30.0.5 + babel-jest: 30.1.2(@babel/core@7.28.3) + chalk: 4.1.2 + ci-info: 4.3.0 + deepmerge: 4.3.1 + glob: 10.4.5 + graceful-fs: 4.2.11 + jest-circus: 30.1.3(babel-plugin-macros@3.1.0) + jest-docblock: 30.0.1 + jest-environment-node: 30.1.2 + jest-regex-util: 30.0.1 + jest-resolve: 30.1.3 + jest-runner: 30.1.3 + jest-util: 30.0.5 + jest-validate: 30.1.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 30.0.5 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + "@types/node": 16.18.11 + ts-node: 10.9.1(@types/node@16.18.11)(typescript@5.9.2) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-config@30.1.3(@types/node@24.2.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@types/node@16.18.11)(typescript@5.9.2)): + dependencies: + "@babel/core": 7.28.3 + "@jest/get-type": 30.1.0 + "@jest/pattern": 30.0.1 + "@jest/test-sequencer": 30.1.3 + "@jest/types": 30.0.5 + babel-jest: 30.1.2(@babel/core@7.28.3) + chalk: 4.1.2 + ci-info: 4.3.0 + deepmerge: 4.3.1 + glob: 10.4.5 + graceful-fs: 4.2.11 + jest-circus: 30.1.3(babel-plugin-macros@3.1.0) + jest-docblock: 30.0.1 + jest-environment-node: 30.1.2 + jest-regex-util: 30.0.1 + jest-resolve: 30.1.3 + jest-runner: 30.1.3 + jest-util: 30.0.5 + jest-validate: 30.1.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 30.0.5 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + "@types/node": 24.2.1 + ts-node: 10.9.1(@types/node@16.18.11)(typescript@5.9.2) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@30.1.2: + dependencies: + "@jest/diff-sequences": 30.0.1 + "@jest/get-type": 30.1.0 + chalk: 4.1.2 + pretty-format: 30.0.5 + + jest-docblock@30.0.1: + dependencies: + detect-newline: 3.1.0 + + jest-each@30.1.0: + dependencies: + "@jest/get-type": 30.1.0 + "@jest/types": 30.0.5 + chalk: 4.1.2 + jest-util: 30.0.5 + pretty-format: 30.0.5 + + jest-environment-node@30.1.2: + dependencies: + "@jest/environment": 30.1.2 + "@jest/fake-timers": 30.1.2 + "@jest/types": 30.0.5 + "@types/node": 24.2.1 + jest-mock: 30.0.5 + jest-util: 30.0.5 + jest-validate: 30.1.0 + + jest-haste-map@30.1.0: + dependencies: + "@jest/types": 30.0.5 + "@types/node": 24.2.1 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 30.0.1 + jest-util: 30.0.5 + jest-worker: 30.1.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@30.1.0: + dependencies: + "@jest/get-type": 30.1.0 + pretty-format: 30.0.5 + + jest-matcher-utils@30.1.2: + dependencies: + "@jest/get-type": 30.1.0 + chalk: 4.1.2 + jest-diff: 30.1.2 + pretty-format: 30.0.5 + + jest-message-util@30.1.0: + dependencies: + "@babel/code-frame": 7.27.1 + "@jest/types": 30.0.5 + "@types/stack-utils": 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 30.0.5 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@30.0.5: + dependencies: + "@jest/types": 30.0.5 + "@types/node": 24.2.1 + jest-util: 30.0.5 + + jest-pnp-resolver@1.2.3(jest-resolve@30.1.3): + optionalDependencies: + jest-resolve: 30.1.3 + + jest-regex-util@30.0.1: {} + + jest-resolve-dependencies@30.1.3: + dependencies: + jest-regex-util: 30.0.1 + jest-snapshot: 30.1.2 + transitivePeerDependencies: + - supports-color + + jest-resolve@30.1.3: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 30.1.0 + jest-pnp-resolver: 1.2.3(jest-resolve@30.1.3) + jest-util: 30.0.5 + jest-validate: 30.1.0 + slash: 3.0.0 + unrs-resolver: 1.11.1 + + jest-runner@30.1.3: + dependencies: + "@jest/console": 30.1.2 + "@jest/environment": 30.1.2 + "@jest/test-result": 30.1.3 + "@jest/transform": 30.1.2 + "@jest/types": 30.0.5 + "@types/node": 24.2.1 + chalk: 4.1.2 + emittery: 0.13.1 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-docblock: 30.0.1 + jest-environment-node: 30.1.2 + jest-haste-map: 30.1.0 + jest-leak-detector: 30.1.0 + jest-message-util: 30.1.0 + jest-resolve: 30.1.3 + jest-runtime: 30.1.3 + jest-util: 30.0.5 + jest-watcher: 30.1.3 + jest-worker: 30.1.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runtime@30.1.3: + dependencies: + "@jest/environment": 30.1.2 + "@jest/fake-timers": 30.1.2 + "@jest/globals": 30.1.2 + "@jest/source-map": 30.0.1 + "@jest/test-result": 30.1.3 + "@jest/transform": 30.1.2 + "@jest/types": 30.0.5 + "@types/node": 24.2.1 + chalk: 4.1.2 + cjs-module-lexer: 2.1.0 + collect-v8-coverage: 1.0.2 + glob: 10.4.5 + graceful-fs: 4.2.11 + jest-haste-map: 30.1.0 + jest-message-util: 30.1.0 + jest-mock: 30.0.5 + jest-regex-util: 30.0.1 + jest-resolve: 30.1.3 + jest-snapshot: 30.1.2 + jest-util: 30.0.5 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-snapshot@30.1.2: + dependencies: + "@babel/core": 7.28.3 + "@babel/generator": 7.28.0 + "@babel/plugin-syntax-jsx": 7.27.1(@babel/core@7.28.3) + "@babel/plugin-syntax-typescript": 7.27.1(@babel/core@7.28.3) + "@babel/types": 7.28.2 + "@jest/expect-utils": 30.1.2 + "@jest/get-type": 30.1.0 + "@jest/snapshot-utils": 30.1.2 + "@jest/transform": 30.1.2 + "@jest/types": 30.0.5 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.3) + chalk: 4.1.2 + expect: 30.1.2 + graceful-fs: 4.2.11 + jest-diff: 30.1.2 + jest-matcher-utils: 30.1.2 + jest-message-util: 30.1.0 + jest-util: 30.0.5 + pretty-format: 30.0.5 + semver: 7.7.2 + synckit: 0.11.11 + transitivePeerDependencies: + - supports-color + jest-util@29.7.0: dependencies: "@jest/types": 29.6.3 @@ -11152,6 +13228,35 @@ snapshots: graceful-fs: 4.2.11 picomatch: 2.3.1 + jest-util@30.0.5: + dependencies: + "@jest/types": 30.0.5 + "@types/node": 24.2.1 + chalk: 4.1.2 + ci-info: 4.3.0 + graceful-fs: 4.2.11 + picomatch: 4.0.3 + + jest-validate@30.1.0: + dependencies: + "@jest/get-type": 30.1.0 + "@jest/types": 30.0.5 + camelcase: 6.3.0 + chalk: 4.1.2 + leven: 3.1.0 + pretty-format: 30.0.5 + + jest-watcher@30.1.3: + dependencies: + "@jest/test-result": 30.1.3 + "@jest/types": 30.0.5 + "@types/node": 24.2.1 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 30.0.5 + string-length: 4.0.2 + jest-worker@29.7.0: dependencies: "@types/node": 24.2.1 @@ -11159,12 +13264,40 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 + jest-worker@30.1.0: + dependencies: + "@types/node": 24.2.1 + "@ungap/structured-clone": 1.3.0 + jest-util: 30.0.5 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@30.1.3(@types/node@16.18.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@types/node@16.18.11)(typescript@5.9.2)): + dependencies: + "@jest/core": 30.1.3(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@types/node@16.18.11)(typescript@5.9.2)) + "@jest/types": 30.0.5 + import-local: 3.2.0 + jest-cli: 30.1.3(@types/node@16.18.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@types/node@16.18.11)(typescript@5.9.2)) + transitivePeerDependencies: + - "@types/node" + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + jiti@1.21.7: {} jose@4.15.9: {} + js-levenshtein@1.1.6: {} + js-tokens@4.0.0: {} + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + js-yaml@4.1.0: dependencies: argparse: 2.0.1 @@ -11192,6 +13325,8 @@ snapshots: dependencies: minimist: 1.2.8 + json5@2.2.3: {} + jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -11219,6 +13354,8 @@ snapshots: dependencies: language-subtag-registry: 0.3.23 + leven@3.1.0: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -11248,6 +13385,10 @@ snapshots: dependencies: lie: 3.1.1 + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -11256,6 +13397,8 @@ snapshots: lodash.isarguments@3.1.0: {} + lodash.memoize@4.1.2: {} + lodash.merge@4.6.2: {} longest-streak@3.1.0: {} @@ -11266,6 +13409,10 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + lru-cache@6.0.0: dependencies: yallist: 4.0.0 @@ -11282,8 +13429,16 @@ snapshots: dependencies: semver: 6.3.1 + make-dir@4.0.0: + dependencies: + semver: 7.7.2 + make-error@1.3.6: {} + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + math-intrinsics@1.1.0: {} mdast-util-from-markdown@2.0.2: @@ -11591,13 +13746,6 @@ snapshots: mkdirp@1.0.4: {} - mlly@1.7.4: - dependencies: - acorn: 8.15.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.1 - mri@1.2.0: {} ms@2.1.1: {} @@ -11618,13 +13766,13 @@ snapshots: neo-async@2.6.2: {} - next-auth@4.24.11(next@14.2.31(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next-auth@4.24.11(next@14.2.31(@babel/core@7.28.3)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: "@babel/runtime": 7.28.2 "@panva/hkdf": 1.2.1 cookie: 0.7.2 jose: 4.15.9 - next: 14.2.31(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0) + next: 14.2.31(@babel/core@7.28.3)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0) oauth: 0.9.15 openid-client: 5.7.1 preact: 10.27.0 @@ -11638,7 +13786,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next@14.2.31(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0): + next@14.2.31(@babel/core@7.28.3)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0): dependencies: "@next/env": 14.2.31 "@swc/helpers": 0.5.5 @@ -11648,7 +13796,7 @@ snapshots: postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.1(react@18.3.1) + styled-jsx: 5.1.1(@babel/core@7.28.3)(babel-plugin-macros@3.1.0)(react@18.3.1) optionalDependencies: "@next/swc-darwin-arm64": 14.2.31 "@next/swc-darwin-x64": 14.2.31 @@ -11664,13 +13812,9 @@ snapshots: - "@babel/core" - babel-plugin-macros - node-abort-controller@3.1.1: {} - node-addon-api@7.1.1: optional: true - node-fetch-native@1.6.7: {} - node-fetch@2.6.7: dependencies: whatwg-url: 5.0.0 @@ -11685,6 +13829,8 @@ snapshots: node-gyp-build@4.8.4: {} + node-int64@0.4.0: {} + node-releases@2.0.19: {} nopt@5.0.0: @@ -11706,21 +13852,12 @@ snapshots: gauge: 3.0.2 set-blocking: 2.0.0 - nuqs@2.4.3(next@14.2.31(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(react@18.3.1): + nuqs@2.4.3(next@14.2.31(@babel/core@7.28.3)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0))(react@18.3.1): dependencies: mitt: 3.0.1 react: 18.3.1 optionalDependencies: - next: 14.2.31(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0) - - nypm@0.5.4: - dependencies: - citty: 0.1.6 - consola: 3.4.2 - pathe: 2.0.3 - pkg-types: 1.3.1 - tinyexec: 0.3.2 - ufo: 1.6.1 + next: 14.2.31(@babel/core@7.28.3)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.90.0) oauth@0.9.15: {} @@ -11770,8 +13907,6 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 - ohash@1.1.6: {} - oidc-token-hash@5.1.1: {} once@1.3.3: @@ -11786,6 +13921,28 @@ snapshots: dependencies: mimic-fn: 2.1.0 + openapi-fetch@0.14.0: + dependencies: + openapi-typescript-helpers: 0.0.15 + + openapi-react-query@0.5.0(@tanstack/react-query@5.85.9(react@18.3.1))(openapi-fetch@0.14.0): + dependencies: + "@tanstack/react-query": 5.85.9(react@18.3.1) + openapi-fetch: 0.14.0 + openapi-typescript-helpers: 0.0.15 + + openapi-typescript-helpers@0.0.15: {} + + openapi-typescript@7.9.1(typescript@5.9.2): + dependencies: + "@redocly/openapi-core": 1.34.5(supports-color@10.2.0) + ansi-colors: 4.1.3 + change-case: 5.4.4 + parse-json: 8.3.0 + supports-color: 10.2.0 + typescript: 5.9.2 + yargs-parser: 21.1.1 + openid-client@5.7.1: dependencies: jose: 4.15.9 @@ -11812,14 +13969,24 @@ snapshots: p-finally@2.0.1: {} + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + p-locate@5.0.0: dependencies: p-limit: 3.1.0 + p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} parent-module@1.0.1: @@ -11843,6 +14010,12 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse-json@8.3.0: + dependencies: + "@babel/code-frame": 7.27.1 + index-to-position: 1.1.0 + type-fest: 4.41.0 + parse-ms@2.1.0: {} path-browserify@1.0.1: {} @@ -11875,14 +14048,8 @@ snapshots: path-type@4.0.0: {} - pathe@1.1.2: {} - - pathe@2.0.3: {} - pend@1.2.0: {} - perfect-debounce@1.0.0: {} - perfect-freehand@1.2.2: {} picocolors@1.0.0: {} @@ -11897,11 +14064,11 @@ snapshots: pirates@4.0.7: {} - pkg-types@1.3.1: + pkg-dir@4.2.0: dependencies: - confbox: 0.1.8 - mlly: 1.7.4 - pathe: 2.0.3 + find-up: 4.1.0 + + pluralize@8.0.0: {} possible-typed-array-names@1.1.0: {} @@ -11962,6 +14129,12 @@ snapshots: pretty-format@3.8.0: {} + pretty-format@30.0.5: + dependencies: + "@jest/schemas": 30.0.5 + ansi-styles: 5.2.0 + react-is: 18.3.1 + pretty-ms@7.0.1: dependencies: parse-ms: 2.1.0 @@ -11993,6 +14166,8 @@ snapshots: punycode@2.3.1: {} + pure-rand@7.0.1: {} + qr.js@0.0.0: {} queue-microtask@1.2.3: {} @@ -12008,11 +14183,6 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 - rc9@2.1.2: - dependencies: - defu: 6.1.4 - destr: 2.0.5 - react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 @@ -12031,6 +14201,8 @@ snapshots: react-is@16.13.1: {} + react-is@18.3.1: {} + react-markdown@9.1.0(@types/react@18.2.20)(react@18.3.1): dependencies: "@types/hast": 3.0.4 @@ -12118,10 +14290,6 @@ snapshots: dependencies: redis-errors: 1.2.0 - redlock@5.0.0-beta.2: - dependencies: - node-abort-controller: 3.1.1 - redux-thunk@3.1.0(redux@5.0.1): dependencies: redux: 5.0.1 @@ -12165,12 +14333,18 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 + require-directory@2.1.1: {} + require-from-string@2.0.2: {} reraf@1.1.1: {} reselect@5.1.1: {} + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -12339,6 +14513,8 @@ snapshots: transitivePeerDependencies: - supports-color + slash@3.0.0: {} + socket.io-client@4.7.2: dependencies: "@socket.io/component-emitter": 3.1.2 @@ -12359,16 +14535,27 @@ snapshots: source-map-js@1.2.1: {} + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + source-map@0.5.7: {} source-map@0.6.1: {} space-separated-tokens@2.0.2: {} + sprintf-js@1.0.3: {} + sprintf-js@1.1.3: {} stable-hash@0.0.5: {} + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + stacktrace-parser@0.1.11: dependencies: type-fest: 0.7.1 @@ -12396,6 +14583,11 @@ snapshots: streamsearch@1.1.0: {} + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -12477,6 +14669,8 @@ snapshots: strip-bom@3.0.0: {} + strip-bom@4.0.0: {} + strip-final-newline@2.0.0: {} strip-json-comments@3.1.1: {} @@ -12489,10 +14683,13 @@ snapshots: dependencies: inline-style-parser: 0.2.4 - styled-jsx@5.1.1(react@18.3.1): + styled-jsx@5.1.1(@babel/core@7.28.3)(babel-plugin-macros@3.1.0)(react@18.3.1): dependencies: client-only: 0.0.1 react: 18.3.1 + optionalDependencies: + "@babel/core": 7.28.3 + babel-plugin-macros: 3.1.0 stylis@4.2.0: {} @@ -12506,6 +14703,8 @@ snapshots: pirates: 4.0.7 ts-interface-checker: 0.1.13 + supports-color@10.2.0: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -12518,6 +14717,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + synckit@0.11.11: + dependencies: + "@pkgr/core": 0.2.9 + tailwindcss@3.4.17(ts-node@10.9.1(@types/node@16.18.11)(typescript@5.9.2)): dependencies: "@alloc/quick-lru": 5.2.0 @@ -12564,6 +14767,12 @@ snapshots: mkdirp: 1.0.4 yallist: 4.0.0 + test-exclude@6.0.0: + dependencies: + "@istanbuljs/schema": 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -12576,13 +14785,13 @@ snapshots: dependencies: convert-hrtime: 3.0.0 - tinyexec@0.3.2: {} - tinyglobby@0.2.14: dependencies: fdir: 6.4.6(picomatch@4.0.3) picomatch: 4.0.3 + tmpl@1.0.5: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -12603,6 +14812,26 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-jest@29.4.1(@babel/core@7.28.3)(@jest/transform@30.1.2)(@jest/types@30.0.5)(babel-jest@30.1.2(@babel/core@7.28.3))(jest-util@30.0.5)(jest@30.1.3(@types/node@16.18.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@types/node@16.18.11)(typescript@5.9.2)))(typescript@5.9.2): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.8 + jest: 30.1.3(@types/node@16.18.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@types/node@16.18.11)(typescript@5.9.2)) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.2 + type-fest: 4.41.0 + typescript: 5.9.2 + yargs-parser: 21.1.1 + optionalDependencies: + "@babel/core": 7.28.3 + "@jest/transform": 30.1.2 + "@jest/types": 30.0.5 + babel-jest: 30.1.2(@babel/core@7.28.3) + jest-util: 30.0.5 + ts-morph@12.0.0: dependencies: "@ts-morph/common": 0.11.1 @@ -12660,8 +14889,14 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-detect@4.0.8: {} + + type-fest@0.21.3: {} + type-fest@0.7.1: {} + type-fest@4.41.0: {} + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -12717,8 +14952,6 @@ snapshots: udomdiff@1.1.2: {} - ufo@1.6.1: {} - uglify-js@3.19.3: optional: true @@ -12739,8 +14972,6 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - uncrypto@0.1.3: {} - undici-types@7.10.0: {} undici@5.28.4: @@ -12818,6 +15049,8 @@ snapshots: uqr@0.1.2: {} + uri-js-replace@1.0.1: {} + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -12853,6 +15086,12 @@ snapshots: v8-compile-cache-lib@3.0.1: {} + v8-to-istanbul@9.3.0: + dependencies: + "@jridgewell/trace-mapping": 0.3.30 + "@types/istanbul-lib-coverage": 2.0.6 + convert-source-map: 2.0.0 + vercel@37.14.0: dependencies: "@vercel/build-utils": 8.4.12 @@ -12883,6 +15122,10 @@ snapshots: "@types/unist": 3.0.3 vfile-message: 4.0.3 + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + wavesurfer.js@7.10.1: {} web-vitals@0.2.4: {} @@ -12967,6 +15210,11 @@ snapshots: wrappy@1.0.2: {} + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + ws@8.17.1: {} xdg-app-paths@5.1.0: @@ -12979,14 +15227,30 @@ snapshots: xmlhttprequest-ssl@2.0.0: {} + y18n@5.0.8: {} + yallist@3.1.1: {} yallist@4.0.0: {} + yaml-ast-parser@0.0.43: {} + yaml@1.10.2: {} yaml@2.8.1: {} + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yauzl-clone@1.0.4: dependencies: events-intercept: 2.0.0 @@ -13005,4 +15269,6 @@ snapshots: yocto-queue@0.1.0: {} + zod@4.1.5: {} + zwitch@2.0.4: {} diff --git a/www/public/service-worker.js b/www/public/service-worker.js index 109561d5..e798e369 100644 --- a/www/public/service-worker.js +++ b/www/public/service-worker.js @@ -1,4 +1,4 @@ -let authToken = ""; // Variable to store the token +let authToken = null; self.addEventListener("message", (event) => { if (event.data && event.data.type === "SET_AUTH_TOKEN") {