diff --git a/.eslintrc.json b/.eslintrc.json index bffb357a..e474ae86 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,11 @@ { - "extends": "next/core-web-vitals" + "extends": "next/core-web-vitals", + "rules": { + "react-hooks/exhaustive-deps": [ + "warn", + { + "additionalHooks": "(useSignaledEffect)" + } + ] + } } diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index cbe27da3..d4a4f299 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -5,7 +5,10 @@ on: push: branches: - main - +env: + NEXT_PUBLIC_OZONE_SERVICE_DID: did:plc:ar7c4by46qjdydhdevvrndac + NEXT_PUBLIC_PLC_DIRECTORY_URL: https://plc.directory + NEXT_PUBLIC_HANDLE_RESOLVER_URL: https://api.bsky.app jobs: cypress-run: runs-on: ubuntu-22.04 diff --git a/app/actions/ModActionPanel/QuickAction.tsx b/app/actions/ModActionPanel/QuickAction.tsx index d538809b..ebcb6b78 100644 --- a/app/actions/ModActionPanel/QuickAction.tsx +++ b/app/actions/ModActionPanel/QuickAction.tsx @@ -1,5 +1,5 @@ // TODO: This is badly named so that we can rebuild this component without breaking the old one -import { useQuery } from '@tanstack/react-query' +import { useQuery, useQueryClient } from '@tanstack/react-query' import { ComAtprotoModerationDefs, ToolsOzoneModerationDefs, @@ -10,9 +10,7 @@ import { ActionPanel } from '@/common/ActionPanel' import { ButtonPrimary, ButtonSecondary } from '@/common/buttons' import { Checkbox, FormLabel, Input, Textarea } from '@/common/forms' import { PropsOf } from '@/lib/types' -import client from '@/lib/client' import { BlobList } from './BlobList' -import { queryClient } from 'components/QueryClient' import { LabelChip, LabelList, @@ -38,8 +36,8 @@ import { ActionDurationSelector } from '@/reports/ModerationForm/ActionDurationS import { MOD_EVENTS } from '@/mod-event/constants' import { ModEventList } from '@/mod-event/EventList' import { ModEventSelectorButton } from '@/mod-event/SelectorButton' -import { createSubjectFromId } from '@/reports/helpers/subject' import { SubjectReviewStateBadge } from '@/subject/ReviewStateMarker' +import { useCreateSubjectFromId } from '@/reports/helpers/subject' import { getProfileUriForDid } from '@/reports/helpers/subject' import { Dialog } from '@headlessui/react' import { SubjectSwitchButton } from '@/common/SubjectSwitchButton' @@ -50,7 +48,11 @@ import { DM_DISABLE_TAG, VIDEO_UPLOAD_DISABLE_TAG } from '@/lib/constants' import { MessageActorMeta } from '@/dms/MessageActorMeta' import { ModEventDetailsPopover } from '@/mod-event/DetailsPopover' import { LockClosedIcon } from '@heroicons/react/24/solid' -import { checkPermission } from '@/lib/server-config' +import { + useConfigurationContext, + useLabelerAgent, + usePermission, +} from '@/shell/ConfigurationContext' const FORM_ID = 'mod-action-panel' const useBreakpoint = createBreakpoint({ xs: 340, sm: 640 }) @@ -144,7 +146,10 @@ export function ModActionPanelQuick( const getDeactivatedAt = ({ repo, record, -}: Awaited>) => { +}: { + repo?: ToolsOzoneModerationDefs.RepoViewDetail + record?: ToolsOzoneModerationDefs.RecordViewDetail +}) => { const deactivatedAt = repo?.deactivatedAt || record?.repo?.deactivatedAt if (!deactivatedAt) { @@ -160,6 +165,11 @@ function Form( replaceFormWithEvents: boolean } & Pick, ) { + const { config } = useConfigurationContext() + const queryClient = useQueryClient() + const labelerAgent = useLabelerAgent() + const accountDid = labelerAgent.assertDid + const { subject, setSubject, @@ -173,16 +183,13 @@ function Form( isSubmitting: boolean error: string }>({ isSubmitting: false, error: '' }) - const { data: subjectStatus, refetch: refetchSubjectStatus } = useQuery({ - // subject of the report - queryKey: ['modSubjectStatus', { subject }], - queryFn: () => getSubjectStatus(subject), - }) - const { data: { record, repo } = {}, refetch: refetchSubject } = useQuery({ - // subject of the report - queryKey: ['modActionSubject', { subject }], - queryFn: () => getSubject(subject), - }) + + const { data: subjectStatus, refetch: refetchSubjectStatus } = + useSubjectStatusQuery(subject) + + const { data: { record, repo } = {}, refetch: refetchSubject } = + useSubjectQuery(subject) + const isSubjectDid = subject.startsWith('did:') const isReviewClosed = subjectStatus?.reviewState === ToolsOzoneModerationDefs.REVIEWCLOSED @@ -207,7 +214,7 @@ function Form( const deactivatedAt = getDeactivatedAt( repo ? { repo } : record ? { record } : {}, ) - const canManageChat = checkPermission('canManageChat') + const canManageChat = usePermission('canManageChat') // navigate to next or prev report const navigateQueue = (delta: 1 | -1) => { @@ -253,6 +260,9 @@ function Form( window.removeEventListener('keydown', downHandler) } }, []) + + const createSubjectFromId = useCreateSubjectFromId() + // on form submit const onFormSubmit = async ( ev: FormEvent & { target: HTMLFormElement }, @@ -380,7 +390,7 @@ function Form( labelSubmissions.push( onSubmit({ subject: { ...subjectInfo, cid: labelCid }, - createdBy: client.session.did, + createdBy: accountDid, subjectBlobCids: formData .getAll('subjectBlobCids') .map((cid) => String(cid)), @@ -402,7 +412,7 @@ function Form( labelSubmissions.push( onSubmit({ subject: subjectInfo, - createdBy: client.session.did, + createdBy: accountDid, subjectBlobCids: formData .getAll('subjectBlobCids') .map((cid) => String(cid)), @@ -415,7 +425,7 @@ function Form( } else { await onSubmit({ subject: subjectInfo, - createdBy: client.session.did, + createdBy: accountDid, subjectBlobCids, event: coreEvent, }) @@ -424,7 +434,7 @@ function Form( if (formData.get('additionalAcknowledgeEvent')) { await onSubmit({ subject: subjectInfo, - createdBy: client.session.did, + createdBy: accountDid, subjectBlobCids: formData .getAll('subjectBlobCids') .map((cid) => String(cid)), @@ -680,9 +690,8 @@ function Form( name="labels" formId={FORM_ID} defaultLabels={currentLabels.filter((label) => { - const serviceDid = client.getServiceDid()?.split('#')[0] const isExternalLabel = allLabels.some((l) => { - return l.val === label && l.src !== serviceDid + return l.val === label && l.src !== config.did }) return !isSelfLabel(label) && !isExternalLabel })} @@ -830,41 +839,48 @@ function Form( ) } -async function getSubject(subject: string) { - if (subject.startsWith('did:')) { - const { data: repo } = await client.api.tools.ozone.moderation.getRepo( - { - did: subject, - }, - { headers: client.proxyHeaders() }, - ) - return { repo } - } else if (subject.startsWith('at://')) { - const { data: record } = await client.api.tools.ozone.moderation.getRecord( - { - uri: subject, - }, - { headers: client.proxyHeaders() }, - ) - - return { record } - } else { - return {} - } +function useSubjectQuery(subject: string) { + const labelerAgent = useLabelerAgent() + + return useQuery({ + // subject of the report + queryKey: ['modActionSubject', { subject }], + queryFn: async () => { + if (subject.startsWith('did:')) { + const { data: repo } = + await labelerAgent.api.tools.ozone.moderation.getRepo({ + did: subject, + }) + return { repo } + } else if (subject.startsWith('at://')) { + const { data: record } = + await labelerAgent.api.tools.ozone.moderation.getRecord({ + uri: subject, + }) + return { record } + } else { + return {} + } + }, + }) } -async function getSubjectStatus(subject: string) { - const { - data: { subjectStatuses }, - } = await client.api.tools.ozone.moderation.queryStatuses( - { - subject, - includeMuted: true, - limit: 1, +function useSubjectStatusQuery(subject: string) { + const labelerAgent = useLabelerAgent() + return useQuery({ + // subject of the report + queryKey: ['modSubjectStatus', { subject }], + queryFn: async () => { + const { + data: { subjectStatuses }, + } = await labelerAgent.api.tools.ozone.moderation.queryStatuses({ + subject, + includeMuted: true, + limit: 1, + }) + return subjectStatuses.at(0) || null }, - { headers: client.proxyHeaders() }, - ) - return subjectStatuses.at(0) || null + }) } function isMultiPress(ev: KeyboardEvent) { diff --git a/app/actions/[id]/page.tsx b/app/actions/[id]/page.tsx index 28056dae..57a05da6 100644 --- a/app/actions/[id]/page.tsx +++ b/app/actions/[id]/page.tsx @@ -1,21 +1,19 @@ 'use client' import { useQuery } from '@tanstack/react-query' -import client from '@/lib/client' import { Loading, LoadingFailed } from '@/common/Loader' import { EventView } from '@/mod-event/View' +import { useLabelerAgent } from '@/shell/ConfigurationContext' export default function Action({ params }: { params: { id: string } }) { + const labelerAgent = useLabelerAgent() const id = decodeURIComponent(params.id) const { data: action, error } = useQuery({ queryKey: ['action', { id }], queryFn: async () => { - const { data } = await client.api.tools.ozone.moderation.getEvent( - { - id: parseInt(id, 10), - }, - { headers: client.proxyHeaders() }, - ) + const { data } = await labelerAgent.api.tools.ozone.moderation.getEvent({ + id: parseInt(id, 10), + }) return data }, }) diff --git a/app/communication-template/page.tsx b/app/communication-template/page.tsx index d1e3e465..b8ae2a3c 100644 --- a/app/communication-template/page.tsx +++ b/app/communication-template/page.tsx @@ -11,9 +11,8 @@ import { Loading, LoadingFailed } from '@/common/Loader' import { useCommunicationTemplateList } from 'components/communication-template/hooks' import { CommunicationTemplateDeleteConfirmationModal } from 'components/communication-template/delete-confirmation-modal' import { ActionButton, LinkButton } from '@/common/buttons' -import client from '@/lib/client' import { ErrorInfo } from '@/common/ErrorInfo' -import { checkPermission } from '@/lib/server-config' +import { usePermission } from '@/shell/ConfigurationContext' export default function CommunicationTemplatePage() { const { data, error, isLoading } = useCommunicationTemplateList({}) @@ -25,7 +24,9 @@ export default function CommunicationTemplatePage() { : [] useTitle(`Communication Templates`) - if (!checkPermission('canManageTemplates')) { + const canManageTemplates = usePermission('canManageTemplates') + + if (!canManageTemplates) { return ( Sorry, you don{"'"}t have permission to manage communication templates. diff --git a/app/configure/page-content.tsx b/app/configure/page-content.tsx index a4d7e911..74116a5d 100644 --- a/app/configure/page-content.tsx +++ b/app/configure/page-content.tsx @@ -1,14 +1,13 @@ import { useEffect } from 'react' import { useTitle } from 'react-use' -import client from '@/lib/client' -import { useSession } from '@/lib/useSession' import { Tabs, TabView } from '@/common/Tabs' import { LabelerConfig } from 'components/config/Labeler' import { MemberConfig } from 'components/config/Member' import { ModActionPanelQuick } from 'app/actions/ModActionPanel/QuickAction' import { ToolsOzoneModerationEmitEvent } from '@atproto/api' -import { emitEvent } from '@/mod-event/helpers/emitEvent' +import { useEmitEvent } from '@/mod-event/helpers/emitEvent' import { usePathname, useSearchParams, useRouter } from 'next/navigation' +import { useConfigurationContext } from '@/shell/ConfigurationContext' import { WorkspacePanel } from '@/workspace/Panel' import { useWorkspaceOpener } from '@/common/useWorkspaceOpener' @@ -24,11 +23,12 @@ const TabKeys = { export default function ConfigurePageContent() { useTitle('Configure') - const session = useSession() - useEffect(() => { - client.reconfigure() // Ensure config is up to date - }, []) - const isServiceAccount = !!session && session?.did === session?.config.did + + const { reconfigure } = useConfigurationContext() + useEffect(() => void reconfigure(), [reconfigure]) // Ensure config is up to date + + const emitEvent = useEmitEvent() + const searchParams = useSearchParams() const router = useRouter() const pathname = usePathname() @@ -54,7 +54,6 @@ export default function ConfigurePageContent() { router.push((pathname ?? '') + '?' + newParams.toString()) } - if (!session) return null const views: TabView[] = [ { view: Views.Configure, @@ -74,9 +73,7 @@ export default function ConfigurePageContent() { views={views} fullWidth /> - {currentView === Views.Configure && ( - - )} + {currentView === Views.Configure && } {currentView === Views.Members && } { - const { data } = await client.api.tools.ozone.moderation.getEvent( - { - id: parseInt(id, 10), - }, - { headers: client.proxyHeaders() }, - ) + const { data } = await labelerAgent.api.tools.ozone.moderation.getEvent({ + id: parseInt(id, 10), + }) return data }, }) diff --git a/app/events/page-content.tsx b/app/events/page-content.tsx index bf5a3298..5c749f7e 100644 --- a/app/events/page-content.tsx +++ b/app/events/page-content.tsx @@ -1,6 +1,6 @@ import { useTitle } from 'react-use' import { ModEventList } from '@/mod-event/EventList' -import { emitEvent } from '@/mod-event/helpers/emitEvent' +import { useEmitEvent } from '@/mod-event/helpers/emitEvent' import { ToolsOzoneModerationEmitEvent } from '@atproto/api' import { ModActionPanelQuick } from 'app/actions/ModActionPanel/QuickAction' import { usePathname, useRouter, useSearchParams } from 'next/navigation' @@ -8,6 +8,7 @@ import { WorkspacePanel } from '@/workspace/Panel' import { useWorkspaceOpener } from '@/common/useWorkspaceOpener' export default function EventListPageContent() { + const emitEvent = useEmitEvent() const searchParams = useSearchParams() const router = useRouter() const pathname = usePathname() diff --git a/app/layout.tsx b/app/layout.tsx index 7186e39d..bfc34705 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -5,12 +5,17 @@ import 'yet-another-react-lightbox/styles.css' import 'yet-another-react-lightbox/plugins/thumbnails.css' import 'yet-another-react-lightbox/plugins/captions.css' import { ToastContainer } from 'react-toastify' -import { QueryClientProvider } from '@tanstack/react-query' + import { Shell } from '@/shell/Shell' import { CommandPaletteRoot } from '@/shell/CommandPalette/Root' import { AuthProvider } from '@/shell/AuthContext' -import { queryClient } from 'components/QueryClient' +import { DefaultQueryClientProvider } from '@/shell/QueryClient' +import { GlobalQueryClientProvider } from '@/shell/QueryClient' import { isDarkModeEnabled } from '@/common/useColorScheme' +import { HANDLE_RESOLVER_URL, PLC_DIRECTORY_URL } from '@/lib/constants' +import { ConfigProvider } from '@/shell/ConfigContext' +import { ConfigurationProvider } from '@/shell/ConfigurationContext' +import { ExternalLabelersProvider } from '@/shell/ExternalLabelersContext' export default function RootLayout({ children, @@ -45,13 +50,25 @@ export default function RootLayout({ hideProgressBar={false} closeOnClick /> - - - - {children} - - - + + + + + + + + + {children} + + + + + + + ) diff --git a/app/oauth-client.json/route.ts b/app/oauth-client.json/route.ts new file mode 100644 index 00000000..18b0b284 --- /dev/null +++ b/app/oauth-client.json/route.ts @@ -0,0 +1,40 @@ +import { oauthClientMetadataSchema } from '@atproto/oauth-types' + +import { OAUTH_SCOPE } from '@/lib/constants' + +const logoUrl = '/img/logo-colorful.png' + +export async function GET(request: Request) { + const requestUrl = new URL(request.url) + + const proto = request.headers.get('x-forwarded-proto') + const host = request.headers.get('x-forwarded-host') + if (proto && host) { + const { protocol, hostname, port } = new URL(`${proto}://${host}`) + requestUrl.protocol = protocol + requestUrl.hostname = hostname + requestUrl.port = port + } + + return Response.json( + oauthClientMetadataSchema.parse({ + client_id: requestUrl.href, + client_uri: new URL('/', requestUrl).href, + redirect_uris: [new URL('/', requestUrl).href], + response_types: ['code'], + grant_types: ['authorization_code'], + token_endpoint_auth_method: 'none', + scope: OAUTH_SCOPE, + dpop_bound_access_tokens: true, + application_type: 'web', + client_name: 'Ozone Service', + logo_uri: new URL( + `/_next/image?url=${encodeURIComponent(logoUrl)}&w=150&q=75`, + requestUrl, + ).href, + // tos_uri: 'https://example.com/tos', + // policy_uri: 'https://example.com/policy', + // jwks_uri: 'https://example.com/jwks', + }), + ) +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 00000000..e65f9e64 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,14 @@ +'use client' + +import { useRouter } from 'next/navigation' +import { Suspense, useEffect } from 'react' + +export default function Home() { + const router = useRouter() + + useEffect(() => { + router.push('/reports?resolved=false') + }, [router]) + + return }> +} diff --git a/app/reports/page-content.tsx b/app/reports/page-content.tsx index f595e2fe..38dce881 100644 --- a/app/reports/page-content.tsx +++ b/app/reports/page-content.tsx @@ -1,5 +1,5 @@ 'use client' -import { useContext, useCallback } from 'react' +import { useCallback } from 'react' import { ReadonlyURLSearchParams, usePathname, @@ -15,17 +15,17 @@ import { } from '@atproto/api' import { SectionHeader } from '../../components/SectionHeader' import { ModActionIcon } from '@/common/ModActionIcon' -import client from '@/lib/client' import { validSubjectString } from '@/lib/types' -import { emitEvent } from '@/mod-event/helpers/emitEvent' import { ModActionPanelQuick } from '../actions/ModActionPanel/QuickAction' -import { AuthContext } from '@/shell/AuthContext' import { ButtonGroup } from '@/common/buttons' -import { useFluentReportSearch } from '@/reports/useFluentReportSearch' import { SubjectTable } from 'components/subject/table' import { useTitle } from 'react-use' import { LanguagePicker } from '@/common/LanguagePicker' import { QueueSelector, QUEUE_NAMES } from '@/reports/QueueSelector' +import { unique } from '@/lib/util' +import { useEmitEvent } from '@/mod-event/helpers/emitEvent' +import { useFluentReportSearchParams } from '@/reports/useFluentReportSearch' +import { useLabelerAgent } from '@/shell/ConfigurationContext' import { WorkspacePanel } from 'components/workspace/Panel' import { useWorkspaceOpener } from '@/common/useWorkspaceOpener' @@ -178,19 +178,13 @@ const getSortParams = (params: ReadonlyURLSearchParams) => { } export const ReportsPageContent = () => { + const emitEvent = useEmitEvent() const params = useSearchParams() const quickOpenParam = params.get('quickOpen') ?? '' const takendown = !!params.get('takendown') const includeMuted = !!params.get('includeMuted') - const onlyMuted = !!params.get('onlyMuted') const appealed = params.get('appealed') const reviewState = params.get('reviewState') - const tags = params.get('tags') - const excludeTags = params.get('excludeTags') - const queueName = params.get('queueName') - const { sortField, sortDirection } = getSortParams(params) - const { getReportSearchParams } = useFluentReportSearch() - const { lastReviewedBy, subject, reporters } = getReportSearchParams() const router = useRouter() const pathname = usePathname() const setQuickActionPanelSubject = (subject: string) => { @@ -204,79 +198,9 @@ export const ReportsPageContent = () => { } const { toggleWorkspacePanel, isWorkspaceOpen } = useWorkspaceOpener() - const { isLoggedIn } = useContext(AuthContext) const { data, fetchNextPage, hasNextPage, refetch, isInitialLoading } = - useInfiniteQuery({ - enabled: isLoggedIn, - queryKey: [ - 'events', - { - subject, - sortField, - sortDirection, - reviewState, - lastReviewedBy, - reporters, - takendown, - appealed, - tags, - excludeTags, - queueName, - includeMuted, - onlyMuted, - }, - ], - queryFn: async ({ pageParam }) => { - const queryParams: Parameters[0] = { - cursor: pageParam, - } - - if (subject) { - queryParams.subject = subject - } - - if (takendown) { - queryParams.takendown = takendown - } - - if (includeMuted) { - queryParams.includeMuted = includeMuted - } - - if (onlyMuted) { - queryParams.onlyMuted = onlyMuted - } - - if (appealed) { - // If not specifically set to true but there is a value, we can default to false - // No value will pass undefined which will be ignored - queryParams.appealed = appealed === 'true' - } - - if (tags) { - queryParams.tags = tags.split(',') - } - - if (excludeTags) { - queryParams.excludeTags = excludeTags.split(',') - } + useModerationQueueQuery() - // For these fields, we only want to add them to the filter if the values are set, otherwise, defaults will kick in - Object.entries({ - sortField, - sortDirection, - reviewState, - lastReviewedBy, - }).forEach(([key, value]) => { - if (value) { - queryParams[key] = value - } - }) - - return await getModerationQueue(queryParams, queueName) - }, - getNextPageParam: (lastPage) => lastPage.cursor, - }) const subjectStatuses = data?.pages.flatMap((page) => page.subjectStatuses) ?? [] const currentTab = getTabFromParams({ reviewState }) @@ -351,39 +275,110 @@ function getTabFromParams({ reviewState }: { reviewState?: string | null }) { return 'all' } -async function getModerationQueue( - opts: ToolsOzoneModerationQueryStatuses.QueryParams = {}, - queueName: string | null, -) { - const { data } = await client.api.tools.ozone.moderation.queryStatuses( - { - limit: 50, - includeMuted: true, - ...opts, - }, - { headers: client.proxyHeaders() }, - ) +function useModerationQueueQuery() { + const labelerAgent = useLabelerAgent() + const params = useSearchParams() - const queueDivider = QUEUE_NAMES.length - const queueIndex = QUEUE_NAMES.indexOf(queueName ?? '') - const statusesInQueue = queueName - ? data.subjectStatuses.filter((status) => { - const subjectDid = - status.subject.$type === 'com.atproto.admin.defs#repoRef' - ? status.subject.did - : new AtUri(`${status.subject.uri}`).host - const queueDeciderCharCode = - `${subjectDid}`.split(':').pop()?.charCodeAt(0) || 0 - return queueDeciderCharCode % queueDivider === queueIndex + const takendown = !!params.get('takendown') + const includeMuted = !!params.get('includeMuted') + const onlyMuted = !!params.get('onlyMuted') + const appealed = params.get('appealed') + const reviewState = params.get('reviewState') + const tags = params.get('tags') + const excludeTags = params.get('excludeTags') + const queueName = params.get('queueName') + const { sortField, sortDirection } = getSortParams(params) + const { lastReviewedBy, subject, reporters } = useFluentReportSearchParams() + + return useInfiniteQuery({ + queryKey: [ + 'events', + { + subject, + sortField, + sortDirection, + reviewState, + lastReviewedBy, + reporters, + takendown, + appealed, + tags, + excludeTags, + queueName, + includeMuted, + onlyMuted, + }, + ], + queryFn: async ({ pageParam }) => { + const queryParams: ToolsOzoneModerationQueryStatuses.QueryParams = { + cursor: pageParam, + } + + if (subject) { + queryParams.subject = subject + } + + if (takendown) { + queryParams.takendown = takendown + } + + if (includeMuted) { + queryParams.includeMuted = includeMuted + } + + if (onlyMuted) { + queryParams.onlyMuted = onlyMuted + } + + if (appealed) { + // If not specifically set to true but there is a value, we can default to false + // No value will pass undefined which will be ignored + queryParams.appealed = appealed === 'true' + } + + if (tags) { + queryParams.tags = tags.split(',') + } + + if (excludeTags) { + queryParams.excludeTags = excludeTags.split(',') + } + + // For these fields, we only want to add them to the filter if the values are set, otherwise, defaults will kick in + Object.entries({ + sortField, + sortDirection, + reviewState, + lastReviewedBy, + }).forEach(([key, value]) => { + if (value) { + queryParams[key] = value + } }) - : data.subjectStatuses - return { cursor: data.cursor, subjectStatuses: statusesInQueue } -} + const { data } = + await labelerAgent.api.tools.ozone.moderation.queryStatuses({ + limit: 50, + includeMuted: true, + ...queryParams, + }) -function unique(arr: T[]) { - const set = new Set(arr) - const result: T[] = [] - set.forEach((val) => result.push(val)) - return result + const queueDivider = QUEUE_NAMES.length + const queueIndex = QUEUE_NAMES.indexOf(queueName ?? '') + const statusesInQueue = queueName + ? data.subjectStatuses.filter((status) => { + const subjectDid = + status.subject.$type === 'com.atproto.admin.defs#repoRef' + ? status.subject.did + : new AtUri(`${status.subject.uri}`).host + const queueDeciderCharCode = + `${subjectDid}`.split(':').pop()?.charCodeAt(0) || 0 + return queueDeciderCharCode % queueDivider === queueIndex + }) + : data.subjectStatuses + + return { cursor: data.cursor, subjectStatuses: statusesInQueue } + }, + getNextPageParam: (lastPage) => lastPage.cursor, + }) } diff --git a/app/repositories/[id]/[...record]/page-content.tsx b/app/repositories/[id]/[...record]/page-content.tsx index f06cf259..0c3e5943 100644 --- a/app/repositories/[id]/[...record]/page-content.tsx +++ b/app/repositories/[id]/[...record]/page-content.tsx @@ -4,19 +4,20 @@ import { AppBskyFeedGetPostThread as GetPostThread, ToolsOzoneModerationEmitEvent, } from '@atproto/api' -import { ReportPanel } from '@/reports/ReportPanel' -import { RecordView } from '@/repositories/RecordView' -import client from '@/lib/client' -import { createAtUri } from '@/lib/util' -import { createReport } from '@/repositories/createReport' -import { Loading, LoadingFailed } from '@/common/Loader' -import { CollectionId } from '@/reports/helpers/subject' import { usePathname, useRouter, useSearchParams } from 'next/navigation' -import { ModActionPanelQuick } from 'app/actions/ModActionPanel/QuickAction' -import { emitEvent } from '@/mod-event/helpers/emitEvent' import { useEffect } from 'react' import { useTitle } from 'react-use' + +import { Loading, LoadingFailed } from '@/common/Loader' import { getDidFromHandle } from '@/lib/identity' +import { createAtUri } from '@/lib/util' +import { useEmitEvent } from '@/mod-event/helpers/emitEvent' +import { ReportPanel } from '@/reports/ReportPanel' +import { CollectionId } from '@/reports/helpers/subject' +import { RecordView } from '@/repositories/RecordView' +import { useCreateReport } from '@/repositories/createReport' +import { useLabelerAgent } from '@/shell/ConfigurationContext' +import { ModActionPanelQuick } from 'app/actions/ModActionPanel/QuickAction' import { useWorkspaceOpener } from '@/common/useWorkspaceOpener' import { WorkspacePanel } from '@/workspace/Panel' @@ -54,6 +55,10 @@ export default function RecordViewPageContent({ }: { params: { id: string; record: string[] } }) { + const labelerAgent = useLabelerAgent() + + const emitEvent = useEmitEvent() + const createReport = useCreateReport() const id = decodeURIComponent(params.id) const collection = params.record[0] && decodeURIComponent(params.record[0]) const rkey = params.record[1] && decodeURIComponent(params.record[1]) @@ -78,10 +83,7 @@ export default function RecordViewPageContent({ const uri = createAtUri({ did, collection, rkey }) const getRecord = async () => { const { data: record } = - await client.api.tools.ozone.moderation.getRecord( - { uri }, - { headers: client.proxyHeaders() }, - ) + await labelerAgent.api.tools.ozone.moderation.getRecord({ uri }) return record } const getThread = async () => { @@ -89,10 +91,8 @@ export default function RecordViewPageContent({ return undefined } try { - const { data: thread } = await client.api.app.bsky.feed.getPostThread( - { uri }, - { headers: client.proxyHeaders() }, - ) + const { data: thread } = + await labelerAgent.api.app.bsky.feed.getPostThread({ uri }) return thread } catch (err) { if (err instanceof GetPostThread.NotFoundError) { @@ -106,12 +106,10 @@ export default function RecordViewPageContent({ return undefined } // TODO: We need pagination here, right? how come getPostThread doesn't need it? - const { data: listData } = await client.api.app.bsky.graph.getList( - { + const { data: listData } = + await labelerAgent.api.app.bsky.graph.getList({ list: uri, - }, - { headers: client.proxyHeaders() }, - ) + }) return listData.items.map(({ subject }) => subject) } const [record, profiles, thread] = await Promise.allSettled([ @@ -132,7 +130,7 @@ export default function RecordViewPageContent({ const pathname = usePathname() const quickOpenParam = searchParams.get('quickOpen') ?? '' const reportUri = searchParams.get('reportUri') || undefined - const setQuickActionPanelSubject = (subject: string) => { + const setQuickActionPanelSubject = (subject?: string) => { // This route should not have any search params but in case it does, let's make sure original params are maintained const newParams = new URLSearchParams(searchParams) if (!subject) { @@ -157,6 +155,7 @@ export default function RecordViewPageContent({ if (reportUri === 'default' && data?.record) { setReportUri(data?.record.uri) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [data, reportUri]) const pageTitle = buildPageTitle({ @@ -176,7 +175,7 @@ export default function RecordViewPageContent({ <> setQuickActionPanelSubject('')} + onClose={() => setQuickActionPanelSubject()} setSubject={setQuickActionPanelSubject} subject={quickOpenParam} // select first subject if there are multiple subjectOptions={[quickOpenParam]} diff --git a/app/repositories/[id]/page-content.tsx b/app/repositories/[id]/page-content.tsx index 325bacd3..1aeee1dc 100644 --- a/app/repositories/[id]/page-content.tsx +++ b/app/repositories/[id]/page-content.tsx @@ -1,15 +1,17 @@ 'use client' -import { AccountView } from '@/repositories/AccountView' -import { createReport } from '@/repositories/createReport' -import { useRepoAndProfile } from '@/repositories/useRepoAndProfile' + import { ToolsOzoneModerationEmitEvent } from '@atproto/api' -import { ModActionPanelQuick } from 'app/actions/ModActionPanel/QuickAction' import { usePathname, useRouter, useSearchParams } from 'next/navigation' -import { emitEvent } from '@/mod-event/helpers/emitEvent' import { useTitle } from 'react-use' import { useWorkspaceOpener } from '@/common/useWorkspaceOpener' import { WorkspacePanel } from '@/workspace/Panel' +import { useEmitEvent } from '@/mod-event/helpers/emitEvent' +import { AccountView } from '@/repositories/AccountView' +import { useCreateReport } from '@/repositories/createReport' +import { useRepoAndProfile } from '@/repositories/useRepoAndProfile' +import { ModActionPanelQuick } from 'app/actions/ModActionPanel/QuickAction' + const buildPageTitle = ({ handle, tab, @@ -38,6 +40,9 @@ export function RepositoryViewPageContent({ id }: { id: string }) { refetch, isLoading: isInitialLoading, } = useRepoAndProfile({ id }) + + const createReport = useCreateReport() + const emitEvent = useEmitEvent() const searchParams = useSearchParams() const router = useRouter() const pathname = usePathname() diff --git a/app/repositories/page-content.tsx b/app/repositories/page-content.tsx index 616e046b..4f378672 100644 --- a/app/repositories/page-content.tsx +++ b/app/repositories/page-content.tsx @@ -2,84 +2,81 @@ import { SectionHeader } from '../../components/SectionHeader' import { RepositoriesTable } from '@/repositories/RepositoriesTable' import { useSearchParams } from 'next/navigation' import { useInfiniteQuery } from '@tanstack/react-query' -import client from '@/lib/client' import { useTitle } from 'react-use' import { ToolsOzoneModerationDefs } from '@atproto/api' +import { useLabelerAgent } from '@/shell/ConfigurationContext' const isEmailSearch = (q: string) => q.startsWith('email:') -const getSearchResults = async ({ - q, - cursor, -}: { - q: string - cursor?: string -}): Promise<{ - cursor?: string - repos: ToolsOzoneModerationDefs.RepoView[] -}> => { - const headers = { headers: client.proxyHeaders() } - const limit = 25 - - if (!isEmailSearch(q)) { - const { data } = await client.api.tools.ozone.moderation.searchRepos( - { q, limit, cursor }, - headers, - ) - - return data - } - - const email = q.replace('email:', '').trim() - - if (!email) { - return { repos: [], cursor: undefined } - } - - const { data } = await client.api.com.atproto.admin.searchAccounts( - { email, limit, cursor }, - headers, - ) +function useSearchResultsQuery(q: string) { + const labelerAgent = useLabelerAgent() - if (!data.accounts.length) { - return { repos: [], cursor: data.cursor } - } - - const repos: Record = {} - data.accounts.forEach((account) => { - repos[account.did] = { - ...account, - // Set placeholder properties that will be later filled in with data from ozone - relatedRecords: [], - indexedAt: account.indexedAt, - moderation: {}, - labels: [], - } - }) - - await Promise.allSettled( - data.accounts.map(async (account) => { - const { data } = await client.api.tools.ozone.moderation.getRepo( - { did: account.did }, - headers, + return useInfiniteQuery({ + queryKey: ['repositories', { q }], + queryFn: async ({ pageParam }) => { + const limit = 25 + + if (!isEmailSearch(q)) { + const { data } = + await labelerAgent.api.tools.ozone.moderation.searchRepos({ + q, + limit, + cursor: pageParam, + }) + + return data + } + + const email = q.replace('email:', '').trim() + + if (!email) { + return { repos: [], cursor: undefined } + } + + const { data } = await labelerAgent.api.com.atproto.admin.searchAccounts({ + email, + limit, + cursor: pageParam, + }) + + if (!data.accounts.length) { + return { repos: [], cursor: data.cursor } + } + + const repos: Record = {} + data.accounts.forEach((account) => { + repos[account.did] = { + ...account, + // Set placeholder properties that will be later filled in with data from ozone + relatedRecords: [], + indexedAt: account.indexedAt, + moderation: {}, + labels: [], + } + }) + + await Promise.allSettled( + data.accounts.map(async (account) => { + const { data } = + await labelerAgent.api.tools.ozone.moderation.getRepo({ + did: account.did, + }) + repos[account.did] = { ...repos[account.did], ...data } + }), ) - repos[account.did] = { ...repos[account.did], ...data } - }), - ) - return { repos: Object.values(repos), cursor: data.cursor } + return { repos: Object.values(repos), cursor: data.cursor } + }, + getNextPageParam: (lastPage) => lastPage.cursor, + }) } export default function RepositoriesListPage() { const params = useSearchParams() + const q = params.get('term') ?? '' - const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({ - queryKey: ['repositories', { q }], - queryFn: async ({ pageParam }) => { - return getSearchResults({ q, cursor: pageParam }) - }, - getNextPageParam: (lastPage) => lastPage.cursor, - }) + + const { data, fetchNextPage, hasNextPage } = useSearchResultsQuery(q) let pageTitle = `Repositories` if (q) { diff --git a/app/subject-status/page.tsx b/app/subject-status/page.tsx index 157c9886..426c9e9d 100644 --- a/app/subject-status/page.tsx +++ b/app/subject-status/page.tsx @@ -1,14 +1,18 @@ 'use client' -import { Loading, LoadingFailed } from '@/common/Loader' +import { useLabelerAgent } from '@/shell/ConfigurationContext' import { useSearchParams } from 'next/navigation' -import { SubjectStatusView } from '@/subject/StatusView' import { useTitle } from 'react-use' import { useSubjectStatus } from '@/subject/useSubjectStatus' +import { Loading, LoadingFailed } from '@/common/Loader' +import { SubjectStatusView } from '@/subject/StatusView' + export default function SubjectStatus() { const params = useSearchParams() + const labelerAgent = useLabelerAgent() + const subject = params.get('uri') || params.get('did') - const { data, status, error } = useSubjectStatus({ subject }) + const { data, isLoading, error } = useSubjectStatus({ subject }) let pageTitle = `Subject Status` @@ -18,7 +22,7 @@ export default function SubjectStatus() { useTitle(pageTitle) - if (status === 'loading') { + if (isLoading) { return } diff --git a/components/QueryClient.tsx b/components/QueryClient.tsx deleted file mode 100644 index 9f692601..00000000 --- a/components/QueryClient.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { QueryClient } from '@tanstack/react-query' - -export const queryClient = new QueryClient() diff --git a/components/common/RecordCard.tsx b/components/common/RecordCard.tsx index 67a78362..fbdcd205 100644 --- a/components/common/RecordCard.tsx +++ b/components/common/RecordCard.tsx @@ -6,7 +6,6 @@ import { ComAtprotoLabelDefs, } from '@atproto/api' import { buildBlueSkyAppUrl, parseAtUri } from '@/lib/util' -import client from '@/lib/client' import { PostAsCard } from './posts/PostsFeed' import Link from 'next/link' import { LoadingDense, displayError, LoadingFailedDense } from './Loader' @@ -16,6 +15,7 @@ import { FeedGeneratorRecordCard } from './feeds/RecordCard' import { ProfileAvatar } from '@/repositories/ProfileAvatar' import { ShieldCheckIcon } from '@heroicons/react/24/solid' import { StarterPackRecordCard } from './starterpacks/RecordCard' +import { useLabelerAgent } from '@/shell/ConfigurationContext' export function RecordCard(props: { uri: string; showLabels?: boolean }) { const { uri, showLabels = false } = props @@ -53,19 +53,16 @@ export function RecordCard(props: { uri: string; showLabels?: boolean }) { ) } -function PostCard(props: { uri: string; showLabels?: boolean }) { - const { uri, showLabels } = props +function PostCard({ uri, showLabels }: { uri: string; showLabels?: boolean }) { + const labelerAgent = useLabelerAgent() + const { error, data } = useQuery({ retry: false, queryKey: ['postCard', { uri }], queryFn: async () => { // @TODO when unifying admin auth, ensure admin can see taken-down posts - const { data: post } = await client.api.app.bsky.feed.getPostThread( - { - uri, - depth: 0, - }, - { headers: client.proxyHeaders() }, + const { data: post } = await labelerAgent.api.app.bsky.feed.getPostThread( + { uri, depth: 0 }, ) return post }, @@ -126,21 +123,24 @@ function PostCard(props: { uri: string; showLabels?: boolean }) { ) } -function BaseRecordCard(props: { +function BaseRecordCard({ + uri, + renderRecord, +}: { uri: string renderRecord: ( record: ToolsOzoneModerationDefs.RecordViewDetail, ) => JSX.Element }) { - const { uri, renderRecord } = props + const labelerAgent = useLabelerAgent() + const { data: record, error } = useQuery({ retry: false, queryKey: ['recordCard', { uri }], queryFn: async () => { - const { data } = await client.api.tools.ozone.moderation.getRecord( - { uri }, - { headers: client.proxyHeaders() }, - ) + const { data } = await labelerAgent.api.tools.ozone.moderation.getRecord({ + uri, + }) return data }, }) @@ -178,26 +178,26 @@ function GenericRecordCard({ } const useRepoAndProfile = ({ did }: { did: string }) => { + const labelerAgent = useLabelerAgent() + return useQuery({ retry: false, queryKey: ['repoCard', { did }], queryFn: async () => { // @TODO when unifying admin auth, ensure admin can see taken-down profiles const getRepo = async () => { - const { data: repo } = await client.api.tools.ozone.moderation.getRepo( - { did }, - { headers: client.proxyHeaders() }, - ) + const { data: repo } = + await labelerAgent.api.tools.ozone.moderation.getRepo({ + did, + }) return repo } const getProfile = async () => { try { - const { data: profile } = await client.api.app.bsky.actor.getProfile( - { + const { data: profile } = + await labelerAgent.api.app.bsky.actor.getProfile({ actor: did, - }, - { headers: client.proxyHeaders() }, - ) + }) return profile } catch (err) { if (err?.['status'] === 400) { diff --git a/components/common/SetupModal.tsx b/components/common/SetupModal.tsx new file mode 100644 index 00000000..e043cde6 --- /dev/null +++ b/components/common/SetupModal.tsx @@ -0,0 +1,49 @@ +import Image from 'next/image' +import { ComponentProps, ReactNode } from 'react' + +import { Loading } from './Loader' +import { classNames } from '@/lib/util' + +export type Props = { + title?: ReactNode + children?: ReactNode +} & ComponentProps<'div'> + +export function SetupModal({ + title, + children = , + className, + ...props +}: Props) { + return ( +
+
+
+
+ Ozone - Bluesky Admin +

+ Bluesky Admin Tools +

+ {title && ( +

+ {title} +

+ )} +
+ + {children} +
+
+
+ ) +} diff --git a/components/common/Tabs.tsx b/components/common/Tabs.tsx index a61afef3..ddc159b4 100644 --- a/components/common/Tabs.tsx +++ b/components/common/Tabs.tsx @@ -1,4 +1,12 @@ import { classNames } from '@/lib/util' +import { + HTMLAttributes, + Key, + ReactNode, + useCallback, + useEffect, + useState, +} from 'react' export type TabView = { view: ViewName @@ -66,3 +74,64 @@ function Tab({ ) } + +export type TabsPanelProps = { + views: (TabView & { content: ReactNode })[] + currentView?: ViewName + onCurrentView?: (v: ViewName) => void + autoHide?: boolean + fallback?: ReactNode +} & HTMLAttributes + +export function TabsPanel({ + views, + fallback, + currentView: currentViewExternal, + autoHide = false, + onCurrentView, + ...props +}: TabsPanelProps) { + const available = views.filter((v) => v.content) + const defaultView = available[0]?.view as ViewName | undefined + + const [currentViewInternal, setCurrentViewInternal] = useState(defaultView) + const setCurrent = useCallback( + (v: ViewName) => { + setCurrentViewInternal(v) + onCurrentView?.(v) + }, + [onCurrentView], + ) + const current = + (currentViewExternal != null + ? available.find((v) => v.view === currentViewExternal) + : undefined) ?? + (currentViewInternal != null + ? available.find((v) => v.view === currentViewInternal) + : undefined) ?? + available[0] + + useEffect(() => { + if (current?.view !== currentViewExternal) onCurrentView?.(current?.view) + }, [current?.view, currentViewExternal, onCurrentView]) + + useEffect(() => { + setCurrentViewInternal(current?.view) + }, [current?.view]) + + return ( +
+ {autoHide && available.length <= 1 ? null : ( + + )} +
+ {current?.content ?? fallback} +
+
+ ) +} diff --git a/components/common/feeds/AuthorFeed.tsx b/components/common/feeds/AuthorFeed.tsx index b3e246ad..f67fd753 100644 --- a/components/common/feeds/AuthorFeed.tsx +++ b/components/common/feeds/AuthorFeed.tsx @@ -1,68 +1,93 @@ 'use client' import { useInfiniteQuery } from '@tanstack/react-query' import { Posts } from '../posts/Posts' -import client from '@/lib/client' import { useState } from 'react' import { useRepoAndProfile } from '@/repositories/useRepoAndProfile' import { AppBskyFeedDefs } from '@atproto/api' import { TypeFilterKey, TypeFiltersByKey } from '../posts/constants' +import { useLabelerAgent } from '@/shell/ConfigurationContext' -const getAuthorFeed = async ({ id, pageParam, typeFilter, options }) => { - const limit = 30 - let filteredFeed: AppBskyFeedDefs.FeedViewPost[] = [] - let cursor = pageParam - const isPostFilter = [ - TypeFiltersByKey.posts_no_replies.key, - TypeFiltersByKey.posts_with_media.key, - ].includes(typeFilter) - const isQuoteOrRepostFilter = [ - TypeFiltersByKey.reposts.key, - TypeFiltersByKey.quotes.key, - TypeFiltersByKey.quotes_and_reposts.key, - ].includes(typeFilter) +export const useAuthorFeedQuery = ({ + id, + query, + typeFilter, +}: { + id: string + query: string + typeFilter: TypeFilterKey +}) => { + const { data: repoData } = useRepoAndProfile({ id }) + const labelerAgent = useLabelerAgent() + + return useInfiniteQuery({ + queryKey: ['authorFeed', { id, query, typeFilter }], + queryFn: async ({ pageParam }) => { + const searchPosts = query.length && repoData?.repo.handle + if (searchPosts) { + const { data } = await labelerAgent.api.app.bsky.feed.searchPosts({ + q: `from:${repoData?.repo.handle} ${query}`, + limit: 30, + cursor: pageParam, + }) + return { ...data, feed: data.posts.map((post) => ({ post })) } + } - while (filteredFeed.length < limit) { - const { data } = await client.api.app.bsky.feed.getAuthorFeed( - { - limit, - actor: id, - cursor, - ...(isPostFilter ? { filter: typeFilter } : {}), - }, - options, - ) + const limit = 30 + let filteredFeed: AppBskyFeedDefs.FeedViewPost[] = [] + let cursor = pageParam + const isPostFilter = [ + TypeFiltersByKey.posts_no_replies.key, + TypeFiltersByKey.posts_with_media.key, + ].includes(typeFilter) + const isQuoteOrRepostFilter = [ + TypeFiltersByKey.reposts.key, + TypeFiltersByKey.quotes.key, + TypeFiltersByKey.quotes_and_reposts.key, + ].includes(typeFilter) - // Only repost/quote post filters are applied on the client side - if (!isQuoteOrRepostFilter) { - return data - } + while (filteredFeed.length < limit) { + const { data } = await labelerAgent.api.app.bsky.feed.getAuthorFeed({ + limit, + actor: id, + cursor, + ...(isPostFilter ? { filter: typeFilter } : {}), + }) - const newFilteredItems = data.feed.filter((item) => { - const isRepost = item.reason?.$type === 'app.bsky.feed.defs#reasonRepost' - const isQuotePost = - item.post.embed?.$type === 'app.bsky.embed.record#view' - if (typeFilter === TypeFiltersByKey.reposts.key) { - return isRepost - } - if (typeFilter === TypeFiltersByKey.quotes.key) { - // When a quoted post is reposted, we don't want to consider that a quote post - return isQuotePost && !isRepost - } - return isRepost || isQuotePost - }) + // Only repost/quote post filters are applied on the client side + if (!isQuoteOrRepostFilter) { + return data + } + + const newFilteredItems = data.feed.filter((item) => { + const isRepost = + item.reason?.$type === 'app.bsky.feed.defs#reasonRepost' + const isQuotePost = + item.post.embed?.$type === 'app.bsky.embed.record#view' + if (typeFilter === TypeFiltersByKey.reposts.key) { + return isRepost + } + if (typeFilter === TypeFiltersByKey.quotes.key) { + // When a quoted post is reposted, we don't want to consider that a quote post + return isQuotePost && !isRepost + } + return isRepost || isQuotePost + }) - filteredFeed = [...filteredFeed, ...newFilteredItems] + filteredFeed = [...filteredFeed, ...newFilteredItems] - // If no more items are available, break the loop to prevent infinite requests - if (!data.cursor) { - break - } + // If no more items are available, break the loop to prevent infinite requests + if (!data.cursor) { + break + } - cursor = data.cursor - } + cursor = data.cursor + } - // Ensure the feed is exactly 30 items if there are more than 30 - return { feed: filteredFeed, cursor } + // Ensure the feed is exactly 30 items if there are more than 30 + return { feed: filteredFeed, cursor } + }, + getNextPageParam: (lastPage) => lastPage.cursor, + }) } export function AuthorFeed({ @@ -74,33 +99,11 @@ export function AuthorFeed({ }) { const [query, setQuery] = useState('') const [typeFilter, setTypeFilter] = useState('no_filter') - const { data: repoData } = useRepoAndProfile({ id }) - const { data, fetchNextPage, hasNextPage, isFetching } = useInfiniteQuery({ - queryKey: ['authorFeed', { id, query, typeFilter }], - queryFn: async ({ pageParam }) => { - const options = { headers: client.proxyHeaders() } - const searchPosts = query.length && repoData?.repo.handle - if (searchPosts) { - const { data } = await client.api.app.bsky.feed.searchPosts( - { - q: `from:${repoData?.repo.handle} ${query}`, - limit: 30, - cursor: pageParam, - }, - options, - ) - return { ...data, feed: data.posts.map((post) => ({ post })) } - } else { - const data = await getAuthorFeed({ - id, - pageParam, - typeFilter, - options, - }) - return data - } - }, - getNextPageParam: (lastPage) => lastPage.cursor, + + const { data, fetchNextPage, hasNextPage, isFetching } = useAuthorFeedQuery({ + id, + query, + typeFilter, }) const items = data?.pages.flatMap((page) => page.feed) ?? [] diff --git a/components/common/feeds/Likes.tsx b/components/common/feeds/Likes.tsx index ece09611..6407fd32 100644 --- a/components/common/feeds/Likes.tsx +++ b/components/common/feeds/Likes.tsx @@ -1,13 +1,15 @@ -import client from '@/lib/client' import { AccountsGrid } from '@/repositories/AccountView' import { useInfiniteQuery } from '@tanstack/react-query' import { LoadMoreButton } from '../LoadMoreButton' +import { usePdsAgent } from '@/shell/AuthContext' const useLikes = (uri: string, cid?: string) => { + const pdsAgent = usePdsAgent() + return useInfiniteQuery({ queryKey: ['likes', { uri, cid }], queryFn: async ({ pageParam }) => { - const { data } = await client.api.app.bsky.feed.getLikes({ + const { data } = await pdsAgent.api.app.bsky.feed.getLikes({ uri, cid, limit: 50, diff --git a/components/common/feeds/RecordCard.tsx b/components/common/feeds/RecordCard.tsx index 6263d9e0..144c03dd 100644 --- a/components/common/feeds/RecordCard.tsx +++ b/components/common/feeds/RecordCard.tsx @@ -1,20 +1,19 @@ import { useQuery } from '@tanstack/react-query' import Link from 'next/link' + import { Loading, LoadingFailed } from '@/common/Loader' -import client from '@/lib/client' import { buildBlueSkyAppUrl } from '@/lib/util' +import { useLabelerAgent } from '@/shell/ConfigurationContext' export const FeedGeneratorRecordCard = ({ uri }: { uri: string }) => { + const labelerAgent = useLabelerAgent() const { error, data, isFetching } = useQuery({ retry: false, - queryKey: ['feed-generator', uri], + queryKey: ['feed-generator', { uri }], queryFn: async () => { - const { data } = await client.api.app.bsky.feed.getFeedGenerator( - { - feed: uri, - }, - { headers: client.proxyHeaders() }, - ) + const { data } = await labelerAgent.api.app.bsky.feed.getFeedGenerator({ + feed: uri, + }) return data }, }) @@ -91,9 +90,7 @@ export const FeedGeneratorRecordCard = ({ uri }: { uri: string }) => {
{description &&

{description}

} - {!!meta.length && ( -

{meta.join(' - ')}

- )} + {!!meta.length &&

{meta.join(' - ')}

}
) diff --git a/components/common/feeds/Reposts.tsx b/components/common/feeds/Reposts.tsx index 6440ac5b..0b98395b 100644 --- a/components/common/feeds/Reposts.tsx +++ b/components/common/feeds/Reposts.tsx @@ -1,13 +1,14 @@ -import client from '@/lib/client' import { AccountsGrid } from '@/repositories/AccountView' import { useInfiniteQuery } from '@tanstack/react-query' import { LoadMoreButton } from '../LoadMoreButton' +import { usePdsAgent } from '@/shell/AuthContext' const useReposts = (uri: string, cid?: string) => { + const pdsAgent = usePdsAgent() return useInfiniteQuery({ queryKey: ['reposts', { uri, cid }], queryFn: async ({ pageParam }) => { - const { data } = await client.api.app.bsky.feed.getRepostedBy({ + const { data } = await pdsAgent.api.app.bsky.feed.getRepostedBy({ uri, cid, limit: 50, diff --git a/components/common/labels/List.tsx b/components/common/labels/List.tsx index 8ae6dc3e..a84e73a4 100644 --- a/components/common/labels/List.tsx +++ b/components/common/labels/List.tsx @@ -12,7 +12,7 @@ import { TagIcon, } from '@heroicons/react/24/outline' import { ComponentProps, Fragment } from 'react' -import { useLabelerServiceDef } from './useLabelerDefinition' +import { useLabelerDefinitionQuery } from './useLabelerDefinition' import { isSelfLabel, toLabelVal } from './util' export function LabelList(props: ComponentProps<'div'>) { @@ -91,7 +91,7 @@ export const ModerationLabel = ({ label: ComAtprotoLabelDefs.Label recordAuthorDid?: string } & ComponentProps<'span'>) => { - const labelerServiceDef = useLabelerServiceDef(label.src) + const { data: labelerServiceDef } = useLabelerDefinitionQuery(label.src) const isFromCurrentService = label.src === OZONE_SERVICE_DID const labelVal = toLabelVal(label, recordAuthorDid) diff --git a/components/common/labels/Selector.tsx b/components/common/labels/Selector.tsx index 9b3cbe36..123df752 100644 --- a/components/common/labels/Selector.tsx +++ b/components/common/labels/Selector.tsx @@ -1,6 +1,9 @@ -import { useState } from 'react' -import { ALL_LABELS, getCustomLabels } from './util' +import { useMemo, useState } from 'react' + +import { unique } from '@/lib/util' +import { useConfigurationContext } from '@/shell/ConfigurationContext' import { Input } from '../forms' +import { ALL_LABELS } from './util' const EMPTY_ARR = [] @@ -13,25 +16,27 @@ export const LabelSelector = (props: LabelsProps) => { disabled, onChange, } = props + const { config } = useConfigurationContext() const [query, setQuery] = useState('') - const [selectedLabels, setSelectedLabels] = useState( - defaultLabels.map((label) => label), - ) - const selectorOptions = Array.from( - new Set([ - ...getCustomLabels(), - ...Object.values(ALL_LABELS).map(({ identifier }) => identifier), - ...selectedLabels, - ]), + const [selectedLabels, setSelectedLabels] = useState(defaultLabels) + + const selectorOptions = useMemo( + () => + unique([ + ...(config?.labeler?.policies.labelValues || []), + ...Object.values(ALL_LABELS).map(({ identifier }) => identifier), + ...selectedLabels, + ]) + // If there's a query string, filter to only show the labels that match the query + // this is also used to show a message when no labels are found to allow the user + // add the custom input as a label + .filter((label) => { + if (!query) return true + return label.toLowerCase().includes(query.toLowerCase()) + }) + .sort((prev, next) => prev.localeCompare(next)), + [config, query, selectedLabels], ) - // If there's a query string, filter to only show the labels that match the query - // this is also used to show a message when no labels are found to allow the user - // add the custom input as a label - .filter((label) => { - if (!query) return true - return label.toLowerCase().includes(query.toLowerCase()) - }) - .sort((prev, next) => prev.localeCompare(next)) // Function to toggle label selection const toggleLabel = (label) => { diff --git a/components/common/labels/useLabelerDefinition.ts b/components/common/labels/useLabelerDefinition.ts index 35933127..2ce4f7c0 100644 --- a/components/common/labels/useLabelerDefinition.ts +++ b/components/common/labels/useLabelerDefinition.ts @@ -1,16 +1,18 @@ -import clientManager from '@/lib/client' import { ComAtprotoLabelDefs } from '@atproto/api' import { useQuery } from '@tanstack/react-query' import { ExtendedLabelerServiceDef } from './util' +import { usePdsAgent } from '@/shell/AuthContext' -export const useLabelerServiceDef = (did: string) => { - const { data: labelerDef } = useQuery({ +export const useLabelerDefinitionQuery = (did: string) => { + const pdsAgent = usePdsAgent() + + return useQuery({ queryKey: ['labelerDef', { did }], queryFn: async () => { - if (!did) { + if (!did?.startsWith('did:')) { return null } - const { data } = await clientManager.api.app.bsky.labeler.getServices({ + const { data } = await pdsAgent.api.app.bsky.labeler.getServices({ dids: [did], detailed: true, }) @@ -37,6 +39,4 @@ export const useLabelerServiceDef = (did: string) => { staleTime: 60 * 60 * 1000, cacheTime: 60 * 60 * 1000, }) - - return labelerDef || null } diff --git a/components/common/labels/util.ts b/components/common/labels/util.ts index a7be9e5c..cf6d2f71 100644 --- a/components/common/labels/util.ts +++ b/components/common/labels/util.ts @@ -1,5 +1,3 @@ -import client from '@/lib/client' -import { unique } from '@/lib/util' import { AppBskyActorDefs, AppBskyLabelerDefs, @@ -9,12 +7,11 @@ import { } from '@atproto/api' export type ExtendedLabelerServiceDef = - | (AppBskyLabelerDefs.LabelerViewDetailed & { + | AppBskyLabelerDefs.LabelerViewDetailed & { policies: AppBskyLabelerDefs.LabelerViewDetailed['policies'] & { definitionById: Record } - }) - | null + } export function diffLabels(current: string[], next: string[]) { return { @@ -103,14 +100,3 @@ export const getLabelsForSubject = ({ }) => { return record?.labels ?? repo?.labels ?? [] } - -export const getCustomLabels = () => - client.session?.config.labeler?.policies.labelValues || [] - -export const buildAllLabelOptions = ( - defaultLabels: string[], - options: string[], -) => { - const customLabels = getCustomLabels() - return unique([...defaultLabels, ...options, ...(customLabels || [])]).sort() -} diff --git a/components/common/starterpacks/RecordCard.tsx b/components/common/starterpacks/RecordCard.tsx index 25918e4d..cd0a426e 100644 --- a/components/common/starterpacks/RecordCard.tsx +++ b/components/common/starterpacks/RecordCard.tsx @@ -1,22 +1,20 @@ import { useQuery } from '@tanstack/react-query' import Link from 'next/link' import { Loading, LoadingFailed } from '@/common/Loader' -import client from '@/lib/client' import { buildBlueSkyAppUrl, parseAtUri } from '@/lib/util' import { AppBskyGraphDefs } from '@atproto/api' import { SOCIAL_APP_URL, STARTER_PACK_OG_CARD_URL } from '@/lib/constants' +import { useLabelerAgent } from '@/shell/ConfigurationContext' export const StarterPackRecordCard = ({ uri }: { uri: string }) => { + const lablerAgent = useLabelerAgent() const { error, data, isFetching } = useQuery({ retry: false, queryKey: ['starterpack', uri], queryFn: async () => { - const { data } = await client.api.app.bsky.graph.getStarterPack( - { - starterPack: uri, - }, - { headers: client.proxyHeaders() }, - ) + const { data } = await lablerAgent.api.app.bsky.graph.getStarterPack({ + starterPack: uri, + }) return data }, }) diff --git a/components/communication-template/delete-confirmation-modal.tsx b/components/communication-template/delete-confirmation-modal.tsx index 3b8cdf91..34d20b21 100644 --- a/components/communication-template/delete-confirmation-modal.tsx +++ b/components/communication-template/delete-confirmation-modal.tsx @@ -1,10 +1,10 @@ import { Dialog, Transition } from '@headlessui/react' +import { useQueryClient } from '@tanstack/react-query' import { Fragment, useState } from 'react' +import { toast } from 'react-toastify' import { ActionButton } from '@/common/buttons' -import clientManager from '@/lib/client' -import { queryClient } from 'components/QueryClient' -import { toast } from 'react-toastify' +import { useLabelerAgent } from '@/shell/ConfigurationContext' export const CommunicationTemplateDeleteConfirmationModal = ({ setIsDialogOpen, @@ -14,6 +14,9 @@ export const CommunicationTemplateDeleteConfirmationModal = ({ templateId?: string }) => { const [isDeleting, setIsDeleting] = useState(false) + const labelerAgent = useLabelerAgent() + const queryClient = useQueryClient() + if (!templateId) { return null } @@ -21,10 +24,9 @@ export const CommunicationTemplateDeleteConfirmationModal = ({ const onDelete = async () => { setIsDeleting(true) try { - await clientManager.api.tools.ozone.communication.deleteTemplate( - { id: templateId }, - { headers: clientManager.proxyHeaders(), encoding: 'application/json' }, - ) + await labelerAgent.api.tools.ozone.communication.deleteTemplate({ + id: templateId, + }) toast.success('Template deleted') queryClient.invalidateQueries(['communicationTemplateList']) setIsDeleting(false) diff --git a/components/communication-template/hooks.ts b/components/communication-template/hooks.ts index 77d7d3ee..2780e79b 100644 --- a/components/communication-template/hooks.ts +++ b/components/communication-template/hooks.ts @@ -1,16 +1,16 @@ import { useQuery } from '@tanstack/react-query' - -import client from '@/lib/client' -import { queryClient } from 'components/QueryClient' import { toast } from 'react-toastify' import { useRouter } from 'next/navigation' -import { useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' +import { useLabelerAgent } from '@/shell/ConfigurationContext' export const useCommunicationTemplateList = ({ enabled = true, }: { enabled?: boolean -}) => { +} = {}) => { + const labelerAgent = useLabelerAgent() + return useQuery({ queryKey: ['communicationTemplateList'], enabled, @@ -20,16 +20,15 @@ export const useCommunicationTemplateList = ({ staleTime: 60 * 60 * 1000, queryFn: async () => { const { data } = - await client.api.tools.ozone.communication.listTemplates( - {}, - { headers: client.proxyHeaders() }, - ) + await labelerAgent.api.tools.ozone.communication.listTemplates() return data.communicationTemplates }, }) } export const useCommunicationTemplateEditor = (templateId?: string) => { + const labelerAgent = useLabelerAgent() + const [contentMarkdown, setContentMarkdown] = useState('') const [isSaving, setIsSaving] = useState(false) @@ -41,7 +40,9 @@ export const useCommunicationTemplateEditor = (templateId?: string) => { const router = useRouter() // Enable the query when we have a templateId, otherwise, it means the hook is being mounted on the create page // where we don't need to load any existing template - const { data } = useCommunicationTemplateList({ enabled: !!templateId }) + const { data, refetch } = useCommunicationTemplateList({ + enabled: !!templateId, + }) useEffect(() => { if (templateId && data?.length) { @@ -61,38 +62,35 @@ export const useCommunicationTemplateEditor = (templateId?: string) => { } }, [templateId, data]) - const saveFunc = ({ - contentMarkdown, - name, - subject, - disabled, - }: { - contentMarkdown: string - name: string - subject: string - disabled: boolean - }) => - templateId - ? client.api.tools.ozone.communication.updateTemplate( - { + const saveFunc = useCallback( + async ({ + contentMarkdown, + name, + subject, + disabled, + }: { + contentMarkdown: string + name: string + subject: string + disabled: boolean + }) => + templateId + ? labelerAgent.api.tools.ozone.communication.updateTemplate({ id: `${templateId}`, contentMarkdown, subject, name, disabled, - updatedBy: client.session.did, - }, - { encoding: 'application/json', headers: client.proxyHeaders() }, - ) - : client.api.tools.ozone.communication.createTemplate( - { + updatedBy: labelerAgent.assertDid, + }) + : labelerAgent.api.tools.ozone.communication.createTemplate({ contentMarkdown, subject, name, - createdBy: client.session.did, - }, - { headers: client.proxyHeaders(), encoding: 'application/json' }, - ) + createdBy: labelerAgent.assertDid, + }), + [labelerAgent, templateId], + ) const onSubmit = async (e) => { e.preventDefault() @@ -122,9 +120,7 @@ export const useCommunicationTemplateEditor = (templateId?: string) => { // Reset the form if email is sent successfully e.target.reset() setContentMarkdown('') - queryClient.invalidateQueries({ - queryKey: ['communicationTemplateList'], - }) + refetch() router.push('/communication-template') // On error, we are already showing a generic error message within the toast so // swallowing actual error here and resetting local state back afterwards diff --git a/components/config/Labeler.tsx b/components/config/Labeler.tsx index 7b67db98..4b683684 100644 --- a/components/config/Labeler.tsx +++ b/components/config/Labeler.tsx @@ -3,7 +3,6 @@ import Link from 'next/link' import dynamic from 'next/dynamic' import { useMutation } from '@tanstack/react-query' import { AppBskyLabelerService } from '@atproto/api' -import client, { ClientSession } from '@/lib/client' import { ButtonGroup, ButtonPrimary, ButtonSecondary } from '@/common/buttons' import { Card } from '@/common/Card' import { ErrorInfo } from '@/common/ErrorInfo' @@ -12,21 +11,19 @@ import { isDarkModeEnabled } from '@/common/useColorScheme' import { Checkbox, Textarea } from '@/common/forms' import { ExternalLabelerConfig } from './external-labeler' import { ServerConfig } from './server-config' +import { useConfigurationContext } from '@/shell/ConfigurationContext' +import { usePdsAgent } from '@/shell/AuthContext' const BrowserReactJsonView = dynamic(() => import('react-json-view'), { ssr: false, }) -export function LabelerConfig({ - session, - isServiceAccount, -}: { - session: ClientSession - isServiceAccount: boolean -}) { +export function LabelerConfig() { + const { config, isServiceAccount } = useConfigurationContext() + return (
- {isServiceAccount && } + {isServiceAccount && } {!isServiceAccount && (
@@ -35,21 +32,22 @@ export function LabelerConfig({
- Your service account owner{' '} - {session?.config.handle && {session?.config.handle}} will be - able to see more configuration here. + Your service account owner {config.handle && {config.handle}}{' '} + will be able to see more configuration here.
)} - +
) } -function ConfigureDetails({ session }: { session: ClientSession }) { - const record = session.config.labeler ?? null +function ConfigureDetails() { + const { config } = useConfigurationContext() + + const record = config.labeler return (

@@ -63,26 +61,29 @@ function ConfigureDetails({ session }: { session: ClientSession }) { The existence of a service record makes your service account {' '} available in the Bluesky application, allowing users to choose to use your labeling service. It contains a labeling policy with two parts: -
    -
  • - A list of{' '} - - labelValues - - : all label values that you intend to produce from your labeler. -
  • -
  • - A list of{' '} - - labelValueDefinitions - - : details about how each custom label should be respected by the - Bluesky application and presented to users. -
  • -

- {!record && } - {record && } +
    +
  • + A list of{' '} + + labelValues + + : all label values that you intend to produce from your labeler. +
  • +
  • + A list of{' '} + + labelValueDefinitions + + : details about how each custom label should be respected by the + Bluesky application and presented to users. +
  • +
+ {config.labeler ? ( + + ) : ( + + )}

) @@ -90,9 +91,13 @@ function ConfigureDetails({ session }: { session: ClientSession }) { function RecordInitStep({ repo }: { repo: string }) { const [checked, setChecked] = useState(false) + const pdsAgent = usePdsAgent() + + const { reconfigure } = useConfigurationContext() + const createInitialRecord = useMutation({ mutationFn: async () => { - await client.api.com.atproto.repo.putRecord({ + await pdsAgent.api.com.atproto.repo.putRecord({ repo, collection: 'app.bsky.labeler.service', rkey: 'self', @@ -101,7 +106,7 @@ function RecordInitStep({ repo }: { repo: string }) { policies: { labelValues: [] }, }, }) - await client.reconfigure() + await reconfigure() }, }) return ( @@ -150,6 +155,10 @@ function RecordEditStep({ record: AppBskyLabelerService.Record repo: string }) { + const pdsAgent = usePdsAgent() + + const { reconfigure } = useConfigurationContext() + const [editorMode, setEditorMode] = useState<'json' | 'plain'>('json') const darkMode = isDarkModeEnabled() const [recordVal, setRecordVal] = useSyncedState(record) @@ -164,13 +173,13 @@ function RecordEditStep({ }, [recordVal]) const updateRecord = useMutation({ mutationFn: async () => { - await client.api.com.atproto.repo.putRecord({ + await pdsAgent.api.com.atproto.repo.putRecord({ repo, collection: 'app.bsky.labeler.service', rkey: 'self', record: recordVal, }) - await client.reconfigure() + await reconfigure() }, }) const addLabelValue = () => { diff --git a/components/config/Member.tsx b/components/config/Member.tsx index aae532db..5eba0653 100644 --- a/components/config/Member.tsx +++ b/components/config/Member.tsx @@ -1,6 +1,5 @@ import { ActionButton } from '@/common/buttons' -import client from '@/lib/client' -import { checkPermission } from '@/lib/server-config' +import { usePermission } from '@/shell/ConfigurationContext' import { ToolsOzoneTeamDefs } from '@atproto/api' import { PlusIcon } from '@heroicons/react/24/outline' import { MemberEditor } from 'components/team/MemberEditor' @@ -20,7 +19,7 @@ export function MemberConfig() { setShowMemberCreateForm(false) } } - const canManageTeam = checkPermission('canManageTeam') + const canManageTeam = usePermission('canManageTeam') return (
diff --git a/components/config/data.ts b/components/config/data.ts deleted file mode 100644 index 31a06271..00000000 --- a/components/config/data.ts +++ /dev/null @@ -1,38 +0,0 @@ -import client from '@/lib/client' -import { getLocalStorageData, setLocalStorageData } from '@/lib/local-storage' -import { AppBskyLabelerDefs, ComAtprotoLabelDefs } from '@atproto/api' - -const KEY = 'external_labeler_dids' - -type LabelerDetails = - | (AppBskyLabelerDefs.LabelerViewDetailed & { - policies: AppBskyLabelerDefs.LabelerPolicies & { - definitionById: Record - } - }) - | null -type ExternalLabelers = Record - -export const getExternalLabelers = () => { - const labelers = getLocalStorageData(KEY) - if (!labelers) return {} - return labelers -} - -export const addExternalLabelerDid = (did: string, data: LabelerDetails) => { - const labelers = getExternalLabelers() - if (labelers[did]) return labelers - labelers[did] = data - setLocalStorageData(KEY, labelers) - return labelers -} - -export const removeExternalLabelerDid = (did: string) => { - const labelers = getExternalLabelers() - const serviceDid = client.getServiceDid()?.split('#')[0] - // Don't allow removing original service DID - if (!labelers[did] || serviceDid === did) return labelers - delete labelers[did] - setLocalStorageData(KEY, labelers) - return labelers -} diff --git a/components/config/external-labeler.tsx b/components/config/external-labeler.tsx index 761069c5..4f52cce9 100644 --- a/components/config/external-labeler.tsx +++ b/components/config/external-labeler.tsx @@ -1,20 +1,17 @@ -import { useEffect, useState } from 'react' +import { HTMLAttributes, PropsWithChildren, useEffect, useState } from 'react' import { ActionButton } from '@/common/buttons' import { Card } from '@/common/Card' import { FormLabel, Input } from '@/common/forms' -import { useLabelerServiceDef } from '@/common/labels/useLabelerDefinition' -import { - addExternalLabelerDid, - getExternalLabelers, - removeExternalLabelerDid, -} from './data' +import { useLabelerDefinitionQuery } from '@/common/labels/useLabelerDefinition' import { isDarkModeEnabled } from '@/common/useColorScheme' import dynamic from 'next/dynamic' -import client from '@/lib/client' import { ErrorInfo } from '@/common/ErrorInfo' -import { buildBlueSkyAppUrl } from '@/lib/util' +import { buildBlueSkyAppUrl, classNames } from '@/lib/util' import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/solid' +import { useConfigurationContext } from '@/shell/ConfigurationContext' +import { Loading } from '@/common/Loader' +import { useExternalLabelers } from '@/shell/ExternalLabelersContext' import { RepoFinder } from '@/repositories/Finder' const BrowserReactJsonView = dynamic(() => import('react-json-view'), { @@ -22,17 +19,12 @@ const BrowserReactJsonView = dynamic(() => import('react-json-view'), { }) export const ExternalLabelerConfig = () => { - const [labelers, setLabelers] = useState>({}) - const [did, setDid] = useState('') - const labelerServiceDef = useLabelerServiceDef(did) - const labelerDetails = Object.entries(labelers) - const darkMode = isDarkModeEnabled() - const originalServiceDid = client.getServiceDid()?.split('#')[0] + const { config } = useConfigurationContext() + const [labelers, setLabelers] = useExternalLabelers() + const [did, setDid] = useState('') - useEffect(() => { - setLabelers(getExternalLabelers()) - }, []) - const alreadySubscribed = !!labelerDetails.find(([d]) => d === did) + const { data, error, isLoading } = useLabelerDefinitionQuery(did) + const alreadyPresent = labelers.some((d) => d === did) return ( <> @@ -73,85 +65,124 @@ export const ExternalLabelerConfig = () => { size="sm" appearance="primary" className="px-2 sm:px-4 sm:mr-2 py-1.5" - disabled={!labelerServiceDef || alreadySubscribed} + disabled={ + isLoading || !data || alreadyPresent || did === config.did + } onClick={() => { - const labelers = addExternalLabelerDid(did, labelerServiceDef) - setLabelers(labelers) + setLabelers([...labelers, did]) setDid('') }} > Subscribe
- {did && !labelerServiceDef && ( - Labeler profile not found! + {did && !data && !isLoading && ( + + {String(error || 'Labeler profile not found!')} + )} - {did && alreadySubscribed && ( - + {did && alreadyPresent && ( + You{"'"}re already subscribed to this labeler! )} - - {labelerDetails.length ? ( -
-

Configured labelers

- {labelerDetails.map(([labelerDid, labeler], i) => ( -
-
- - {labeler.creator.displayName} - {' '} - -
-

- {labeler.creator.description} -

-
- -
- {originalServiceDid !== labeler.creator.did && ( - { - const labelers = removeExternalLabelerDid( - labeler.creator.did, - ) - setLabelers(labelers) - }} - > - Unsubscribe - - )} -
- ))} -
- ) : ( -

No external labeler configured

+ {did && did === config.did && ( + + The current service{"'"}s DID cannot be subscribed to as an + external labeler. + )} + +
+

Configured labelers

+ + + + {labelers.map((labelerDid) => ( +
+
+ { + setLabelers(labelers.filter((d) => d !== labelerDid)) + }} + /> +
+ ))} +
) } + +function ExternalLabelerView({ + did, + onUnsubscribe, + className, + children, + ...props +}: HTMLAttributes & { + did: string + onUnsubscribe?: () => void +}) { + const darkMode = isDarkModeEnabled() + const { isLoading, data, error, refetch } = useLabelerDefinitionQuery(did) + + return ( +
+
+ + {data ? data.creator.displayName : did} + {' '} + +
+ {data ? ( + <> +

+ {data.creator.description} +

+ + + + ) : isLoading ? ( + + ) : ( + + {String(error || 'Failed to load.')}{' '} + refetch()} + > + Reload + + . + + )} + + {onUnsubscribe && ( + onUnsubscribe()} + > + Unsubscribe + + )} + {children} +
+ ) +} diff --git a/components/config/server-config.tsx b/components/config/server-config.tsx index 1a26e844..ff70da53 100644 --- a/components/config/server-config.tsx +++ b/components/config/server-config.tsx @@ -1,7 +1,10 @@ import { ActionButton } from '@/common/buttons' import { Card } from '@/common/Card' import { CopyButton } from '@/common/CopyButton' -import client, { ClientSession } from '@/lib/client' +import { + useConfigurationContext, + useServerConfig, +} from '@/shell/ConfigurationContext' import { CheckCircleIcon, XCircleIcon, @@ -10,39 +13,26 @@ import { CloudIcon, ChatBubbleLeftIcon, } from '@heroicons/react/24/solid' -import { useState } from 'react' +import { useMutation } from '@tanstack/react-query' const RefetchConfigButton = () => { - const [isRefetching, setIsRefetching] = useState(false) + const { reconfigure } = useConfigurationContext() + const updateRecord = useMutation({ mutationFn: async () => reconfigure() }) + return ( { - setIsRefetching(true) - await client.refetchServerConfig() - setIsRefetching(false) - window.location.reload() - }} + onClick={() => updateRecord.mutate()} size="xs" - disabled={isRefetching} + disabled={updateRecord.isLoading} appearance="outlined" > - {isRefetching ? 'Refetching...' : 'Refetch'} + {updateRecord.isLoading ? 'Refetching...' : 'Refetch'} ) } -export const ServerConfig = ({ session }: { session: ClientSession }) => { - const config = session.serverConfig - if (!config) { - return ( -
-

- No Server Config Found -

- -
- ) - } +export const ServerConfig = () => { + const config = useServerConfig() return ( <> diff --git a/components/dms/MessageActorMeta.tsx b/components/dms/MessageActorMeta.tsx index bab01e76..07dcb724 100644 --- a/components/dms/MessageActorMeta.tsx +++ b/components/dms/MessageActorMeta.tsx @@ -1,9 +1,10 @@ -import { Alert } from '@/common/Alert' -import { ComponentProps, useState } from 'react' -import client from '@/lib/client' -import { useQuery } from '@tanstack/react-query' -import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/solid' import { ChatBskyModerationGetActorMetadata } from '@atproto/api' +import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/solid' +import { useQuery } from '@tanstack/react-query' +import { ComponentProps, useState } from 'react' + +import { Alert } from '@/common/Alert' +import { useLabelerAgent } from '@/shell/ConfigurationContext' export const useMessageActorMeta = ({ did, @@ -12,6 +13,7 @@ export const useMessageActorMeta = ({ did: string enabled: boolean }) => { + const labelerAgent = useLabelerAgent() // This query is a bit expensive but the data does change relatively frequently so we wanna cache it for only 10m return useQuery< unknown, @@ -24,10 +26,10 @@ export const useMessageActorMeta = ({ enabled, queryKey: ['messageActorMeta', { did }], queryFn: async () => { - const { data } = await client.api.chat.bsky.moderation.getActorMetadata( - { actor: did }, - { headers: client.proxyHeaders() }, - ) + const { data } = + await labelerAgent.api.chat.bsky.moderation.getActorMetadata({ + actor: did, + }) return data }, diff --git a/components/dms/MessageContext.tsx b/components/dms/MessageContext.tsx index d941e8e8..6e747587 100644 --- a/components/dms/MessageContext.tsx +++ b/components/dms/MessageContext.tsx @@ -1,12 +1,15 @@ -import { Alert } from '@/common/Alert' -import { RecordEmbedView } from '@/common/posts/PostsFeed' -import client from '@/lib/client' import { ChatBskyConvoDefs } from '@atproto/api' import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/solid' import { useQuery } from '@tanstack/react-query' import { ComponentProps, useState } from 'react' +import { Alert } from '@/common/Alert' +import { RecordEmbedView } from '@/common/posts/PostsFeed' +import { useLabelerAgent } from '@/shell/ConfigurationContext' + const useMessageContext = ({ messageId, did }) => { + const labelerAgent = useLabelerAgent() + return useQuery({ // Message context isn't likely to change, so cache for a long time cacheTime: 4 * 60 * 60 * 1000, @@ -14,10 +17,10 @@ const useMessageContext = ({ messageId, did }) => { retry: 0, queryKey: ['messageContext', { messageId }], queryFn: async () => { - const { data } = await client.api.chat.bsky.moderation.getMessageContext( - { messageId }, - { headers: client.proxyHeaders() }, - ) + const { data } = + await labelerAgent.api.chat.bsky.moderation.getMessageContext({ + messageId, + }) return data.messages }, }) diff --git a/components/email/Composer.tsx b/components/email/Composer.tsx index 3b154cac..37de9425 100644 --- a/components/email/Composer.tsx +++ b/components/email/Composer.tsx @@ -6,17 +6,18 @@ import dynamic from 'next/dynamic' import { ActionButton } from '@/common/buttons' import { Checkbox, FormLabel, Input, Textarea } from '@/common/forms' -import client from '@/lib/client' -import { compileTemplateContent, getTemplate } from './helpers' -import { useRepoAndProfile } from '@/repositories/useRepoAndProfile' -import { useEmailComposer } from './useComposer' import { useColorScheme } from '@/common/useColorScheme' import { MOD_EVENTS } from '@/mod-event/constants' +import { useRepoAndProfile } from '@/repositories/useRepoAndProfile' +import { useLabelerAgent } from '@/shell/ConfigurationContext' +import { compileTemplateContent, getTemplate } from './helpers' import { TemplateSelector } from './template-selector' +import { useEmailComposer } from './useComposer' const MDEditor = dynamic(() => import('@uiw/react-md-editor'), { ssr: false }) export const EmailComposer = ({ did }: { did: string }) => { + const labelerAgent = useLabelerAgent() const { isSending, requiresConfirmation, @@ -52,19 +53,16 @@ export const EmailComposer = ({ did }: { did: string }) => { .toString() await toast.promise( - client.api.tools.ozone.moderation.emitEvent( - { - event: { - $type: MOD_EVENTS.EMAIL, - comment, - subjectLine: subject, - content: htmlContent, - }, - subject: { $type: 'com.atproto.admin.defs#repoRef', did }, - createdBy: client.session.did, + labelerAgent.api.tools.ozone.moderation.emitEvent({ + event: { + $type: MOD_EVENTS.EMAIL, + comment, + subjectLine: subject, + content: htmlContent, }, - { headers: client.proxyHeaders(), encoding: 'application/json' }, - ), + subject: { $type: 'com.atproto.admin.defs#repoRef', did }, + createdBy: labelerAgent.assertDid, + }), { pending: 'Sending email...', success: { diff --git a/components/list/Lists.tsx b/components/list/Lists.tsx index 70b5129a..4d5014cd 100644 --- a/components/list/Lists.tsx +++ b/components/list/Lists.tsx @@ -1,30 +1,27 @@ import { LabelChip, ModerationLabel } from '@/common/labels' import { Loading } from '@/common/Loader' -import client from '@/lib/client' import { buildBlueSkyAppUrl } from '@/lib/util' +import { useLabelerAgent } from '@/shell/ConfigurationContext' import { useInfiniteQuery } from '@tanstack/react-query' import Link from 'next/link' export function Lists({ actor }: { actor: string }) { - const { data, fetchNextPage, hasNextPage, isInitialLoading } = - useInfiniteQuery({ - queryKey: ['lists', { actor }], - queryFn: async ({ pageParam }) => { - const { data } = await client.api.app.bsky.graph.getLists( - { - actor, - limit: 25, - cursor: pageParam, - }, - { headers: client.proxyHeaders() }, - ) - return data - }, - getNextPageParam: (lastPage) => lastPage.cursor, - }) + const labelerAgent = useLabelerAgent() + const { data, isLoading } = useInfiniteQuery({ + queryKey: ['lists', { actor }], + queryFn: async ({ pageParam }) => { + const { data } = await labelerAgent.api.app.bsky.graph.getLists({ + actor, + limit: 25, + cursor: pageParam, + }) + return data + }, + getNextPageParam: (lastPage) => lastPage.cursor, + }) - if (isInitialLoading) { + if (isLoading) { return (
diff --git a/components/list/RecordCard.tsx b/components/list/RecordCard.tsx index 27c34a15..354c164c 100644 --- a/components/list/RecordCard.tsx +++ b/components/list/RecordCard.tsx @@ -1,21 +1,21 @@ import { useQuery } from '@tanstack/react-query' import Link from 'next/link' + import { Loading, LoadingFailed } from '@/common/Loader' -import client from '@/lib/client' import { buildBlueSkyAppUrl } from '@/lib/util' +import { useLabelerAgent } from '@/shell/ConfigurationContext' export const ListRecordCard = ({ uri }: { uri: string }) => { + const labelerAgent = useLabelerAgent() + const { error, data, isFetching } = useQuery({ retry: false, - queryKey: ['list', uri], + queryKey: ['list', { uri }], queryFn: async () => { - const { data } = await client.api.app.bsky.graph.getList( - { - list: uri, - limit: 1, - }, - { headers: client.proxyHeaders() }, - ) + const { data } = await labelerAgent.api.app.bsky.graph.getList({ + list: uri, + limit: 1, + }) return data }, }) @@ -85,9 +85,7 @@ export const ListRecordCard = ({ uri }: { uri: string }) => {
{description &&

{description}

} - {!!meta.length && ( -

{meta.join(' - ')}

- )} + {!!meta.length &&

{meta.join(' - ')}

}
) diff --git a/components/mod-event/EventItem.tsx b/components/mod-event/EventItem.tsx index 36f5b23c..d4f7e97b 100644 --- a/components/mod-event/EventItem.tsx +++ b/components/mod-event/EventItem.tsx @@ -1,14 +1,15 @@ -import client from '@/lib/client' -import { Card } from '@/common/Card' -import { LabelChip, LabelList, ModerationLabel } from '@/common/labels' -import { ReasonBadge } from '@/reports/ReasonBadge' import { - ToolsOzoneModerationDefs, - ComAtprotoModerationDefs, ChatBskyConvoDefs, + ComAtprotoModerationDefs, + ToolsOzoneModerationDefs, } from '@atproto/api' -import { ItemTitle } from './ItemTitle' + +import { Card } from '@/common/Card' +import { LabelChip, LabelList, ModerationLabel } from '@/common/labels' import { MessageContext } from '@/dms/MessageContext' +import { ReasonBadge } from '@/reports/ReasonBadge' +import { useConfigurationContext } from '@/shell/ConfigurationContext' +import { ItemTitle } from './ItemTitle' import { PreviewCard } from '@/common/PreviewCard' const LinkToAuthor = ({ @@ -203,6 +204,8 @@ const EventLabels = ({ labels?: string[] isTag?: boolean }) => { + const { config } = useConfigurationContext() + if (!labels?.length) return null return ( @@ -218,7 +221,7 @@ const EventLabels = ({ key={label} label={{ val: label, - src: client.getServiceDid() || '', + src: config.did, uri: '', cts: '', }} diff --git a/components/mod-event/ItemTitle.tsx b/components/mod-event/ItemTitle.tsx index 59be021e..da1f2f62 100644 --- a/components/mod-event/ItemTitle.tsx +++ b/components/mod-event/ItemTitle.tsx @@ -110,14 +110,14 @@ export const ItemTitle = ({

{showContentDetails && ( -

+

-

+
)} ) diff --git a/components/mod-event/SelectorButton.tsx b/components/mod-event/SelectorButton.tsx index 505d391b..2d2b1d3c 100644 --- a/components/mod-event/SelectorButton.tsx +++ b/components/mod-event/SelectorButton.tsx @@ -5,7 +5,7 @@ import { Dropdown } from '@/common/Dropdown' import { MOD_EVENTS } from './constants' import { isReporterMuted, isSubjectMuted } from '@/subject/helpers' import { DM_DISABLE_TAG, VIDEO_UPLOAD_DISABLE_TAG } from '@/lib/constants' -import { checkPermission } from '@/lib/server-config' +import { usePermission } from '@/shell/ConfigurationContext' const actions = [ { text: 'Acknowledge', key: MOD_EVENTS.ACKNOWLEDGE }, @@ -75,9 +75,10 @@ export const ModEventSelectorButton = ({ hasBlobs: boolean isSubjectDid: boolean }) => { - const canDivertBlob = checkPermission('canDivertBlob') - const canTakedown = checkPermission('canTakedown') - const canManageChat = checkPermission('canManageChat') + const canDivertBlob = usePermission('canDivertBlob') + const canTakedown = usePermission('canTakedown') + const canManageChat = usePermission('canManageChat') + const availableActions = useMemo(() => { return actions.filter(({ key }) => { // Don't show resolve appeal action if subject is not already in appealed status diff --git a/components/mod-event/helpers/emitEvent.tsx b/components/mod-event/helpers/emitEvent.tsx index 5f7f448c..27453b2a 100644 --- a/components/mod-event/helpers/emitEvent.tsx +++ b/components/mod-event/helpers/emitEvent.tsx @@ -1,118 +1,132 @@ import Link from 'next/link' import { toast } from 'react-toastify' -import client from '@/lib/client' -import { displayError } from '../../common/Loader' -import { queryClient } from 'components/QueryClient' -import { MOD_EVENTS } from '@/mod-event/constants' import { ToolsOzoneModerationEmitEvent } from '@atproto/api' -import { createSubjectFromId } from '@/reports/helpers/subject' +import { useQueryClient } from '@tanstack/react-query' + import { buildItemsSummary, groupSubjects } from '@/workspace/utils' -export const emitEvent = async ( - vals: ToolsOzoneModerationEmitEvent.InputSchema, -) => { - const emitModerationEventAsync = async () => { - const { data } = await client.api.tools.ozone.moderation.emitEvent(vals, { - encoding: 'application/json', - headers: client.proxyHeaders(), - }) +import { displayError } from '../../common/Loader' +import { MOD_EVENTS } from '@/mod-event/constants' +import { useLabelerAgent } from '@/shell/ConfigurationContext' +import { useCallback } from 'react' +import { useCreateSubjectFromId } from '@/reports/helpers/subject' - return data - } +export function useEmitEvent() { + const labelerAgent = useLabelerAgent() + const queryClient = useQueryClient() - try { - const isRecord = vals?.subject.$type === 'com.atproto.repo.strongRef' - await toast.promise(emitModerationEventAsync, { - pending: 'Taking action...', - success: { - render({ data }) { - const eventId = data?.id - const eventType = data?.event.$type as string - const actionTypeString = eventType && eventTexts[eventType] + return useCallback( + async (vals: ToolsOzoneModerationEmitEvent.InputSchema) => { + const emitModerationEventAsync = async () => { + const { data } = + await labelerAgent.api.tools.ozone.moderation.emitEvent(vals) + return data + } - const title = `${isRecord ? 'Record' : 'Repo'} was ${ - actionTypeString ?? 'actioned' - }` + try { + const isRecord = vals?.subject.$type === 'com.atproto.repo.strongRef' + await toast.promise(emitModerationEventAsync, { + pending: 'Taking action...', + success: { + render({ data }) { + const eventId = data?.id + const eventType = data?.event.$type as string + const actionTypeString = eventType && eventTexts[eventType] - return ( -
- {title} -{' '} - - View #{eventId} - -
- ) - }, - }, - }) - if (!isRecord) { - // This may not be all encompassing because in the accountView query, the id may be a did or a handle - queryClient.invalidateQueries(['accountView', { id: vals?.subject.did }]) - } - } catch (err) { - if (err?.['error'] === 'SubjectHasAction') { - toast.warn( - 'We found that subject already has a current action. You may proceed by resolving with that action, or replacing it.', - ) - } else { - toast.error(`Error taking action: ${displayError(err)}`) - } - throw err - } -} + const title = `${isRecord ? 'Record' : 'Repo'} was ${ + actionTypeString ?? 'actioned' + }` -export const actionSubjects = async ( - eventData: Pick< - ToolsOzoneModerationEmitEvent.InputSchema, - 'event' | 'createdBy' - >, - subjects: string[], -) => { - try { - const results: { succeeded: string[]; failed: string[] } = { - succeeded: [], - failed: [], - } - const actions = Promise.allSettled( - subjects.map(async (sub) => { - try { - const { subject } = await createSubjectFromId(sub) - await client.api.tools.ozone.moderation.emitEvent( - { subject, ...eventData }, - { - encoding: 'application/json', - headers: client.proxyHeaders(), + return ( +
+ {title} -{' '} + + View #{eventId} + +
+ ) }, + }, + }) + if (!isRecord) { + // This may not be all encompassing because in the accountView query, the id may be a did or a handle + queryClient.invalidateQueries([ + 'accountView', + { id: vals?.subject.did }, + ]) + } + } catch (err) { + if (err?.['error'] === 'SubjectHasAction') { + toast.warn( + 'We found that subject already has a current action. You may proceed by resolving with that action, or replacing it.', ) - results.succeeded.push(sub) - } catch (err) { - results.failed.push(sub) + } else { + toast.error(`Error taking action: ${displayError(err)}`) + } + throw err + } + }, + [labelerAgent, queryClient], + ) +} + +export const useActionSubjects = () => { + const createSubjectFromId = useCreateSubjectFromId() + const labelerAgent = useLabelerAgent() + + return useCallback( + async ( + eventData: Pick, + subjects: string[], + ) => { + try { + const results: { succeeded: string[]; failed: string[] } = { + succeeded: [], + failed: [], } - }), - ) - await toast.promise(actions, { - pending: `Taking action on ${buildItemsSummary( - groupSubjects(subjects), - )}...`, - success: { - render() { - return results.failed.length - ? `Actioned ${buildItemsSummary( - groupSubjects(results.succeeded), - )}. Failed to action ${buildItemsSummary( - groupSubjects(results.failed), - )}` - : `Actioned ${buildItemsSummary(groupSubjects(results.succeeded))}` - }, - }, - }) - } catch (err) { - toast.error(`Error taking action: ${displayError(err)}`) - throw err - } + const actions = Promise.allSettled( + subjects.map(async (sub) => { + try { + const { subject } = await createSubjectFromId(sub) + await labelerAgent.api.tools.ozone.moderation.emitEvent({ + subject, + createdBy: labelerAgent.assertDid, + ...eventData, + }) + results.succeeded.push(sub) + } catch (err) { + results.failed.push(sub) + } + }), + ) + await toast.promise(actions, { + pending: `Taking action on ${buildItemsSummary( + groupSubjects(subjects), + )}...`, + success: { + render() { + return results.failed.length + ? `Actioned ${buildItemsSummary( + groupSubjects(results.succeeded), + )}. Failed to action ${buildItemsSummary( + groupSubjects(results.failed), + )}` + : `Actioned ${buildItemsSummary( + groupSubjects(results.succeeded), + )}` + }, + }, + }) + } catch (err) { + toast.error(`Error taking action: ${displayError(err)}`) + throw err + } + }, + [labelerAgent, createSubjectFromId], + ) } const eventTexts = { diff --git a/components/mod-event/useModEventList.tsx b/components/mod-event/useModEventList.tsx index 86e3eec3..60f4d040 100644 --- a/components/mod-event/useModEventList.tsx +++ b/components/mod-event/useModEventList.tsx @@ -1,10 +1,10 @@ -import { useInfiniteQuery } from '@tanstack/react-query' -import client from '@/lib/client' -import { useContext, useEffect, useReducer } from 'react' -import { AuthContext } from '@/shell/AuthContext' import { ToolsOzoneModerationQueryEvents } from '@atproto/api' -import { MOD_EVENT_TITLES } from './constants' +import { useInfiniteQuery } from '@tanstack/react-query' import { addDays } from 'date-fns' +import { useEffect, useReducer } from 'react' + +import { useLabelerAgent } from '@/shell/ConfigurationContext' +import { MOD_EVENT_TITLES } from './constants' export type ModEventListQueryOptions = { queryOptions?: { @@ -111,7 +111,7 @@ const eventListReducer = (state: EventListState, action: EventListAction) => { export const useModEventList = ( props: { subject?: string; createdBy?: string } & ModEventListQueryOptions, ) => { - const { isLoggedIn } = useContext(AuthContext) + const labelerAgent = useLabelerAgent() const [listState, dispatch] = useReducer(eventListReducer, initialListState) const setCommentFilter = (value: CommentFilter) => { @@ -135,7 +135,6 @@ export const useModEventList = ( }, [props.createdBy]) const results = useInfiniteQuery({ - enabled: isLoggedIn, queryKey: ['modEventList', { listState }], queryFn: async ({ pageParam }) => { const { @@ -210,7 +209,12 @@ export const useModEventList = ( queryParams.addedTags = removedTags.trim().split(',') } - return await getModerationEvents(queryParams) + const { data } = + await labelerAgent.api.tools.ozone.moderation.queryEvents({ + limit: 25, + ...queryParams, + }) + return data }, getNextPageParam: (lastPage) => lastPage.cursor, ...(props.queryOptions || {}), @@ -262,16 +266,3 @@ export const useModEventList = ( hasFilter, } } - -async function getModerationEvents( - opts: ToolsOzoneModerationQueryEvents.QueryParams = {}, -) { - const { data } = await client.api.tools.ozone.moderation.queryEvents( - { - limit: 25, - ...opts, - }, - { headers: client.proxyHeaders() }, - ) - return data -} diff --git a/components/reports/ReportPanel.tsx b/components/reports/ReportPanel.tsx index a8dcb786..325d128b 100644 --- a/components/reports/ReportPanel.tsx +++ b/components/reports/ReportPanel.tsx @@ -1,11 +1,10 @@ import { useEffect, useState } from 'react' -import { ComAtprotoModerationDefs } from '@atproto/api' import { ActionPanel } from '../common/ActionPanel' import { ButtonPrimary, ButtonSecondary } from '../common/buttons' import { FormLabel, Input, Select, Textarea } from '../common/forms' import { RecordCard, RepoCard } from '../common/RecordCard' import { PropsOf } from '@/lib/types' -import { queryClient } from 'components/QueryClient' +import { useQueryClient } from '@tanstack/react-query' import { SubjectSwitchButton } from '@/common/SubjectSwitchButton' import { reasonTypeOptions } from './helpers/getType' @@ -44,6 +43,7 @@ function Form(props: { } = props const [subject, setSubject] = useState(fixedSubject ?? '') const [submitting, setSubmitting] = useState(false) + const queryClient = useQueryClient() // Update the subject when parent renderer wants to update it // This happens when the subject is loaded async on the renderer component diff --git a/components/reports/helpers/subject.ts b/components/reports/helpers/subject.ts index 78d3e094..36c19f85 100644 --- a/components/reports/helpers/subject.ts +++ b/components/reports/helpers/subject.ts @@ -1,66 +1,69 @@ -import client from '@/lib/client' +import { useLabelerAgent } from '@/shell/ConfigurationContext' import { ToolsOzoneModerationDefs } from '@atproto/api' +import { useCallback } from 'react' export const isIdRecord = (id: string) => id.startsWith('at://') -export const createSubjectFromId = async ( - id: string, -): Promise<{ - subject: { $type: string } & ({ uri: string; cid: string } | { did: string }) - record: ToolsOzoneModerationDefs.RecordViewDetail | null -}> => { - if (isIdRecord(id)) { - try { - const { data: record } = - await client.api.tools.ozone.moderation.getRecord( - { - uri: id, - }, - { headers: client.proxyHeaders() }, - ) - return { - record, - subject: { - $type: 'com.atproto.repo.strongRef', - uri: record.uri, - cid: record.cid, - }, - } - } catch (err) { - if (err?.['error'] === 'RecordNotFound') { - // @TODO this is a roundabout way to get a record cid if the record was deleted. - // It should work pretty well in this context, since createSubjectFromId() is generally used while resolving reports. - const { data: eventData } = - await client.api.tools.ozone.moderation.queryEvents( - { - subject: id, - limit: 1, +export const useCreateSubjectFromId = () => { + const labelerAgent = useLabelerAgent() + + return useCallback( + async ( + id: string, + ): Promise<{ + subject: { $type: string } & ( + | { uri: string; cid: string } + | { did: string } + ) + record: ToolsOzoneModerationDefs.RecordViewDetail | null + }> => { + if (isIdRecord(id)) { + try { + const { data: record } = + await labelerAgent.api.tools.ozone.moderation.getRecord({ uri: id }) + return { + record, + subject: { + $type: 'com.atproto.repo.strongRef', + uri: record.uri, + cid: record.cid, }, - { headers: client.proxyHeaders() }, - ) - const event = eventData.events.at(0) - if (!event || event.subject.uri !== id || !event.subject.cid) { + } + } catch (err) { + if (err?.['error'] === 'RecordNotFound') { + // @TODO this is a roundabout way to get a record cid if the record was deleted. + // It should work pretty well in this context, since createSubjectFromId() is generally used while resolving reports. + const { data: eventData } = + await labelerAgent.api.tools.ozone.moderation.queryEvents({ + subject: id, + limit: 1, + }) + const event = eventData.events.at(0) + if (!event || event.subject.uri !== id || !event.subject.cid) { + throw err + } + return { + record: null, + subject: { + $type: 'com.atproto.repo.strongRef', + uri: event.subject.uri, + cid: `${event.subject.cid}`, + }, + } + } throw err } - return { - record: null, - subject: { - $type: 'com.atproto.repo.strongRef', - uri: event.subject.uri, - cid: `${event.subject.cid}`, - }, - } } - throw err - } - } - return { - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: id, + return { + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: id, + }, + record: null, + } }, - record: null, - } + [labelerAgent], + ) } export enum CollectionId { diff --git a/components/reports/useFluentReportSearch.ts b/components/reports/useFluentReportSearch.ts index d7bfbf4c..ac84c586 100644 --- a/components/reports/useFluentReportSearch.ts +++ b/components/reports/useFluentReportSearch.ts @@ -1,4 +1,5 @@ import { usePathname, useRouter, useSearchParams } from 'next/navigation' +import { useCallback, useMemo } from 'react' type SearchQueryParams = { subject?: string @@ -49,37 +50,42 @@ export const buildParamsFromQuery = ( return params } -export const useFluentReportSearch = () => { +export const useFluentReportSearchUpdate = () => { const router = useRouter() const pathname = usePathname() const params = useSearchParams() - return { - updateParams: (query) => { + return useCallback( + (query) => { const nextParams = new URLSearchParams(params) nextParams.set('term', query) - router.push((pathname ?? '') + '?' + nextParams.toString()) + router.push(`${pathname}?${nextParams}`) }, - getReportSearchParams: (): SearchQueryParams => { - let subject = params.get('term') ?? undefined - const searchParams: SearchQueryParams = { - subject, - lastReviewedBy: undefined, - reporters: undefined, - } + [router, pathname, params], + ) +} - const paramsFromQuery = buildParamsFromQuery(subject) +export const useFluentReportSearchParams = (): SearchQueryParams => { + const subject = useSearchParams().get('term') ?? undefined - // If the params built from query is not empty, that means the term is no longer just subject - if (Object.keys(paramsFromQuery).length) { - searchParams.subject = paramsFromQuery.subject - searchParams.lastReviewedBy = paramsFromQuery.lastReviewedBy - searchParams.reporters = paramsFromQuery.reporters - ? paramsFromQuery.reporters.split(',') - : undefined - } + return useMemo(() => { + const searchParams: SearchQueryParams = { + subject, + lastReviewedBy: undefined, + reporters: undefined, + } - return searchParams - }, - } + const paramsFromQuery = buildParamsFromQuery(subject) + + // If the params built from query is not empty, that means the term is no longer just subject + if (Object.keys(paramsFromQuery).length) { + searchParams.subject = paramsFromQuery.subject + searchParams.lastReviewedBy = paramsFromQuery.lastReviewedBy + searchParams.reporters = paramsFromQuery.reporters + ? paramsFromQuery.reporters.split(',') + : undefined + } + + return searchParams + }, [subject]) } diff --git a/components/repositories/AccountView.tsx b/components/repositories/AccountView.tsx index 74d918fc..da6286f7 100644 --- a/components/repositories/AccountView.tsx +++ b/components/repositories/AccountView.tsx @@ -1,5 +1,5 @@ 'use client' -import { ComponentProps, useEffect, useState } from 'react' +import { ComponentProps, useCallback, useEffect, useState } from 'react' import Link from 'next/link' import { useQuery } from '@tanstack/react-query' import { @@ -18,7 +18,6 @@ import { import { AuthorFeed } from '../common/feeds/AuthorFeed' import { Json } from '../common/Json' import { buildBlueSkyAppUrl, classNames, truncate } from '@/lib/util' -import client from '@/lib/client' import { ReportPanel } from '../reports/ReportPanel' import React from 'react' import { @@ -44,12 +43,12 @@ import { EmptyDataset } from '@/common/feeds/EmptyFeed' import { MuteReporting } from './MuteReporting' import { Tabs, TabView } from '@/common/Tabs' import { Lists } from 'components/list/Lists' +import { useLabelerAgent, usePermission } from '@/shell/ConfigurationContext' import { useWorkspaceAddItemsMutation, useWorkspaceList, useWorkspaceRemoveItemsMutation, } from '@/workspace/hooks' -import { checkPermission } from '@/lib/server-config' import { Blocks } from './Blocks' enum Views { @@ -119,6 +118,8 @@ export function AccountView({ } }, [repo, reportUri]) + const canSendEmail = usePermission('canSendEmail') + const getTabViews = () => { const numInvited = (repo?.invites || []).reduce( (acc, invite) => acc + invite.uses.length, @@ -162,7 +163,7 @@ export function AccountView({ { view: Views.Events, label: 'Events' }, ) - if (checkPermission('canSendEmail')) { + if (canSendEmail) { views.push({ view: Views.Email, label: 'Email' }) } @@ -537,6 +538,7 @@ function Posts({ } function Follows({ id }: { id: string }) { + const labelerAgent = useLabelerAgent() const { error, data: follows, @@ -544,10 +546,9 @@ function Follows({ id }: { id: string }) { } = useQuery({ queryKey: ['follows', { id }], queryFn: async () => { - const { data } = await client.api.app.bsky.graph.getFollows( - { actor: id }, - { headers: client.proxyHeaders() }, - ) + const { data } = await labelerAgent.api.app.bsky.graph.getFollows({ + actor: id, + }) return data }, }) @@ -563,6 +564,7 @@ function Follows({ id }: { id: string }) { } function Followers({ id }: { id: string }) { + const labelerAgent = useLabelerAgent() const { error, isLoading, @@ -570,12 +572,9 @@ function Followers({ id }: { id: string }) { } = useQuery({ queryKey: ['followers', { id }], queryFn: async () => { - const { data } = await client.api.app.bsky.graph.getFollowers( - { - actor: id, - }, - { headers: client.proxyHeaders() }, - ) + const { data } = await labelerAgent.api.app.bsky.graph.getFollowers({ + actor: id, + }) return data }, }) @@ -591,6 +590,7 @@ function Followers({ id }: { id: string }) { } function Invites({ repo }: { repo: GetRepo.OutputSchema }) { + const labelerAgent = useLabelerAgent() const { error, isLoading, @@ -609,27 +609,21 @@ function Invites({ repo }: { repo: GetRepo.OutputSchema }) { if (actors.length === 0) { return { profiles: [] } } - const { data } = await client.api.app.bsky.actor.getProfiles( - { - actors, - }, - { headers: client.proxyHeaders() }, - ) + const { data } = await labelerAgent.api.app.bsky.actor.getProfiles({ + actors, + }) return data }, }) - const onClickRevoke = React.useCallback(async () => { + const onClickRevoke = useCallback(async () => { if (!confirm('Are you sure you want to revoke their invite codes?')) { return } - await client.api.com.atproto.admin.disableInviteCodes( - { - accounts: [repo.did], - }, - { encoding: 'application/json', headers: client.proxyHeaders() }, - ) - }, [client]) + await labelerAgent.api.com.atproto.admin.disableInviteCodes({ + accounts: [repo.did], + }) + }, [labelerAgent, repo.did]) return (
diff --git a/components/repositories/Blocks.tsx b/components/repositories/Blocks.tsx index 20da5009..db725651 100644 --- a/components/repositories/Blocks.tsx +++ b/components/repositories/Blocks.tsx @@ -1,11 +1,13 @@ import { LoadMoreButton } from '@/common/LoadMoreButton' -import client from '@/lib/client' import { AppBskyActorDefs } from '@atproto/api' import { useInfiniteQuery } from '@tanstack/react-query' import { AccountsGrid } from './AccountView' import { listRecords } from './api' +import { useLabelerAgent } from '@/shell/ConfigurationContext' export function Blocks({ did }: { did: string }) { + const labelerAgent = useLabelerAgent() + const { data, error, fetchNextPage, hasNextPage, refetch, isInitialLoading } = useInfiniteQuery({ queryKey: ['blocks', { did }], @@ -20,12 +22,9 @@ export function Blocks({ did }: { did: string }) { return { accounts: [], cursor: null } } const { data: profileData } = - await client.api.app.bsky.actor.getProfiles( - { - actors, - }, - { headers: client.proxyHeaders() }, - ) + await labelerAgent.api.app.bsky.actor.getProfiles({ + actors, + }) const accounts: AppBskyActorDefs.ProfileViewDetailed[] = [] actors.forEach((did) => { diff --git a/components/repositories/Finder.tsx b/components/repositories/Finder.tsx index 9e06e1fe..50680072 100644 --- a/components/repositories/Finder.tsx +++ b/components/repositories/Finder.tsx @@ -1,7 +1,9 @@ -import { useState, useEffect } from 'react' +import { Agent } from '@atproto/api' import { Combobox } from '@headlessui/react' -import client from '@/lib/client' +import { useEffect, useState } from 'react' + import { classNames } from '@/lib/util' +import { useLabelerAgent } from '@/shell/ConfigurationContext' type TypeaheadResult = { did: string @@ -28,15 +30,14 @@ const ErrorResult = { displayName: 'Start typing to search...', } -const getProfilesForQuery = async (q: string): Promise => { - const headers = { headers: client.proxyHeaders() } +const getProfilesForQuery = async ( + agent: Agent, + q: string, +): Promise => { if (q.startsWith('did:')) { - const { data: profile } = await client.api.app.bsky.actor.getProfile( - { - actor: q, - }, - headers, - ) + const { data: profile } = await agent.app.bsky.actor.getProfile({ + actor: q, + }) return profile ? [ @@ -52,7 +53,7 @@ const getProfilesForQuery = async (q: string): Promise => { const { data: { actors }, - } = await client.api.app.bsky.actor.searchActorsTypeahead({ q }, headers) + } = await agent.api.app.bsky.actor.searchActorsTypeahead({ q }) return actors.map((actor) => ({ displayName: actor.displayName, @@ -71,11 +72,12 @@ export function RepoFinder({ const [selectedItem, setSelectedItem] = useState('') const [items, setItems] = useState([DefaultResult]) const [loading, setLoading] = useState(false) + const labelerAgent = useLabelerAgent() useEffect(() => { if (query.length > 0) { setLoading(true) - getProfilesForQuery(query) + getProfilesForQuery(labelerAgent, query) .then((profiles) => { setItems(profiles) setLoading(false) @@ -88,7 +90,7 @@ export function RepoFinder({ } else { setItems([DefaultResult]) } - }, [query]) + }, [labelerAgent, query]) return ( { const queryClient = useQueryClient() + const labelerAgent = useLabelerAgent() + const mutation = useMutation< { success: boolean }, unknown, @@ -25,23 +27,17 @@ const useInviteCodeMutation = ({ did, id }) => { ? 'disableAccountInvites' : 'enableAccountInvites' - const result = await client.api.com.atproto.admin[mutator]( - { account: did, note }, - { - headers: client.proxyHeaders(), - encoding: 'application/json', - }, - ) + const result = await labelerAgent.api.com.atproto.admin[mutator]({ + account: did, + note, + }) // When disabling invites, check if moderator wants to also disable existing codes // If yes, get invite codes through getRepo and disable the active ones if (disableInvites && disableExistingCodes) { - await client.api.com.atproto.admin.disableInviteCodes( - { - accounts: [did], - }, - { encoding: 'application/json', headers: client.proxyHeaders() }, - ) + await labelerAgent.api.com.atproto.admin.disableInviteCodes({ + accounts: [did], + }) } return result @@ -210,11 +206,16 @@ export const InviteCodeGenerationStatus = ({
Invite Code Generation
-
+
} + autoHide + views={[ + { + view: 'credentials', + label: 'Credentials', + content: credentialSignIn ? ( + + ) : undefined, + }, + { + view: 'oauth', + label: 'OAuth', + content: oauthSignIn ? ( + + ) : undefined, + }, + ]} + /> + ) +} diff --git a/components/shell/CommandPalette/useAsyncSearch.tsx b/components/shell/CommandPalette/useAsyncSearch.tsx index 4c54586f..393d4063 100644 --- a/components/shell/CommandPalette/useAsyncSearch.tsx +++ b/components/shell/CommandPalette/useAsyncSearch.tsx @@ -299,7 +299,17 @@ export const useCommandPaletteAsyncSearch = () => { // When we have an exact content by rkey, we want to prioritize it over action for the account by did priority: 6, perform: () => { - router.push(`?quickOpen=${atUri}`) + if (fragments.did) { + return router.push(`?quickOpen=${atUri}`) + } + + const { handle } = fragments + if (handle) { + getDidFromHandle(handle).then((did) => { + if (!did) return + router.push(`?quickOpen=${atUri.replace(handle, did)}`) + }) + } }, }) } @@ -468,7 +478,7 @@ export const useCommandPaletteAsyncSearch = () => { } return actions.map(createAction) - }, [search, didFromHandle, typeaheadResults]) + }, [router, search, didFromHandle, typeaheadResults, kBarQuery]) useRegisterActions(memoizedActions, [search, didFromHandle, typeaheadResults]) } diff --git a/components/shell/ConfigContext.tsx b/components/shell/ConfigContext.tsx new file mode 100644 index 00000000..ac97beee --- /dev/null +++ b/components/shell/ConfigContext.tsx @@ -0,0 +1,75 @@ +'use client' + +import { useQuery } from '@tanstack/react-query' +import { createContext, ReactNode, useContext, useEffect, useMemo } from 'react' +import { useLocalStorage } from 'react-use' + +import { Loading } from '@/common/Loader' +import { SetupModal } from '@/common/SetupModal' +import { getConfig, OzoneConfig } from '@/lib/client-config' +import { GLOBAL_QUERY_CONTEXT } from './QueryClient' + +export type ConfigContextData = { + config: OzoneConfig + configError: Error | null + refetchConfig: () => void +} + +const ConfigContext = createContext(null) + +export const ConfigProvider = ({ children }: { children: ReactNode }) => { + const [cachedConfig, setCachedConfig] = + useLocalStorage('labeler-config') + + const { data, error, refetch } = useQuery({ + // Use the global query client to avoid clearing the cache when the user + // changes. + context: GLOBAL_QUERY_CONTEXT, + retry: (failureCount: number, error: Error): boolean => { + // TODO: change getConfig() to throw a specific error when a network + // error occurs, so we can distinguish between network errors and + // configuration errors. + return false + }, + queryKey: ['labeler-config'], + queryFn: getConfig, + initialData: cachedConfig, + // Refetching will be handled manually + refetchOnWindowFocus: false, + }) + + useEffect(() => { + if (data) setCachedConfig(data) + }, [data, setCachedConfig]) + + const value = useMemo( + () => + data + ? { + config: data, + configError: error, + refetchConfig: refetch, + } + : null, + [data, error, refetch], + ) + + if (!value) { + return ( + + + + ) + } + + return ( + {children} + ) +} + +export function useConfigContext() { + const context = useContext(ConfigContext) + if (context) return context + + throw new Error(`useConfigContext() must be used within a `) +} diff --git a/components/shell/ConfigurationContext.tsx b/components/shell/ConfigurationContext.tsx new file mode 100644 index 00000000..4d6a2895 --- /dev/null +++ b/components/shell/ConfigurationContext.tsx @@ -0,0 +1,166 @@ +'use client' + +import { Agent } from '@atproto/api' +import { + createContext, + ReactNode, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react' + +import { SetupModal } from '@/common/SetupModal' +import { OzoneConfig } from '@/lib/client-config' +import { PermissionName, ServerConfig } from '@/lib/server-config' +import { useAuthContext } from './AuthContext' +import { useConfigContext } from './ConfigContext' +import { useServerConfigQuery } from './ConfigurationContext/useServerConfigQuery' +import { ConfigurationFlow } from './ConfigurationFlow' + +export enum ConfigurationState { + Pending, + Ready, + Unconfigured, + Unauthorized, +} + +export type ConfigurationContextData = { + /** An agent to use in order to communicate with the labeler on the user's behalf. */ + labelerAgent: Agent + isServiceAccount: boolean + config: OzoneConfig + serverConfig: ServerConfig + reconfigure: () => void +} + +const ConfigurationContext = createContext( + null, +) + +export const ConfigurationProvider = ({ + children, +}: { + children: ReactNode +}) => { + // Fetch the labeler static configuration + const { config, configError, refetchConfig } = useConfigContext() + + // Derive an agent for communicating with the labeler, if we have a config and + // an (authenticated) PDS agent. + const { pdsAgent } = useAuthContext() + const labelerAgent = useMemo(() => { + const [did, id = 'atproto_labeler'] = config.did.split('#') + return pdsAgent.withProxy(id, did) + }, [pdsAgent, config.did]) + + // Fetch the user's server configuration + const { + data: serverConfig, + error: serverConfigError, + refetch: refetchServerConfig, + isLoading: isServerConfigLoading, + } = useServerConfigQuery(labelerAgent) + + // Allow ignoring the creation of a record when reconfiguring + const [skipRecord, setSkipRecord] = useState(false) + + // Reset "skipRecord" on credential change + useEffect(() => setSkipRecord(false), [labelerAgent]) + + const accountDid = labelerAgent?.did + + const state = + serverConfigError?.['status'] === 401 + ? ConfigurationState.Unauthorized + : config.needs.key || + config.needs.service || + (config.needs.record && config.did === accountDid && !skipRecord) + ? ConfigurationState.Unconfigured + : !serverConfig + ? isServerConfigLoading + ? ConfigurationState.Pending + : ConfigurationState.Unconfigured + : !serverConfig.role + ? ConfigurationState.Unauthorized + : ConfigurationState.Ready + + const reconfigure = useCallback(async () => { + await refetchConfig() + await refetchServerConfig() + }, [refetchConfig, refetchServerConfig]) + + const configurationContextData = useMemo( + () => + // Note conditions here are redundant, but required for type safety + state === ConfigurationState.Ready && + config && + serverConfig && + labelerAgent + ? { + config, + isServiceAccount: accountDid === config.did, + serverConfig, + labelerAgent, + reconfigure, + } + : null, + [state, accountDid, config, serverConfig, labelerAgent, reconfigure], + ) + + if (!configurationContextData) { + return ( + + setSkipRecord(true)} + createRecord={async () => { + await pdsAgent.com.atproto.repo.putRecord({ + repo: config!.did, + collection: 'app.bsky.labeler.service', + rkey: 'self', + record: { + createdAt: new Date().toISOString(), + policies: { labelValues: [] }, + }, + }) + + await reconfigure() + }} + labelerAgent={labelerAgent} + /> + + ) + } + + return ( + + {children} + + ) +} + +export const useConfigurationContext = () => { + const value = useContext(ConfigurationContext) + if (value) return value + + throw new Error( + `useConfigurationContext() must be used within a `, + ) +} + +export function useLabelerAgent() { + return useConfigurationContext().labelerAgent +} + +export function useServerConfig() { + return useConfigurationContext().serverConfig +} + +export function usePermission(name: PermissionName) { + return useServerConfig().permissions[name] +} diff --git a/components/shell/ConfigurationContext/useServerConfigQuery.ts b/components/shell/ConfigurationContext/useServerConfigQuery.ts new file mode 100644 index 00000000..ee634d83 --- /dev/null +++ b/components/shell/ConfigurationContext/useServerConfigQuery.ts @@ -0,0 +1,71 @@ +import { Agent } from '@atproto/api' +import { ResponseType, XRPCError } from '@atproto/xrpc' +import { useQuery } from '@tanstack/react-query' +import { useEffect } from 'react' +import { useLocalStorage } from 'react-use' + +import { parseServerConfig, ServerConfig } from '@/lib/server-config' + +export function useServerConfigQuery(agent: Agent) { + const [cachedServerConfig, setCachedServerConfig] = + useLocalStorage('labeler-server-config') + + const response = useQuery({ + retry: (failureCount, error): boolean => { + if (error instanceof XRPCError) { + if (error.status === ResponseType.InternalServerError) { + // The server is misconfigured + return false + } + + if ( + error.status === ResponseType.InvalidRequest && + error.message === 'could not resolve proxy did service url' + ) { + // Labeler service not configured in the user's DID document (yet) + return false + } + + if (error.status === ResponseType.AuthRequired) { + // User is logged in with a user that is not member of the labeler's + // group. + return false + } + } + + return failureCount < 3 + }, + retryDelay: (attempt, error) => { + if ( + error instanceof XRPCError && + error.status === ResponseType.RateLimitExceeded && + error.headers?.['ratelimit-remaining'] === '0' && + error.headers?.['ratelimit-reset'] + ) { + // ratelimit-limit: 3000 + // ratelimit-policy: 3000;w=300 + // ratelimit-remaining: 2977 + // ratelimit-reset: 1724927309 + + const reset = Number(error.headers['ratelimit-reset']) * 1e3 + return reset - Date.now() + } + + // Exponential backoff with a maximum of 30 seconds + return Math.min(1000 * 2 ** attempt, 30000) + }, + queryKey: ['server-config', agent.assertDid, agent.proxy], + queryFn: async ({ signal }) => { + const { data } = await agent.tools.ozone.server.getConfig({}, { signal }) + return parseServerConfig(data) + }, + initialData: cachedServerConfig, + refetchOnWindowFocus: false, + }) + + useEffect(() => { + if (response.data) setCachedServerConfig(response.data) + }, [response.data, setCachedServerConfig]) + + return response +} diff --git a/components/shell/ConfigurationFlow.tsx b/components/shell/ConfigurationFlow.tsx index 5375882b..b5ee92c4 100644 --- a/components/shell/ConfigurationFlow.tsx +++ b/components/shell/ConfigurationFlow.tsx @@ -1,27 +1,56 @@ 'use client' -import { ComponentProps, ReactElement, cloneElement, useState } from 'react' -import Link from 'next/link' + import { ArrowLeftOnRectangleIcon, - ArrowRightOnRectangleIcon, ArrowRightCircleIcon, + ArrowRightOnRectangleIcon, } from '@heroicons/react/20/solid' import { useMutation } from '@tanstack/react-query' -import { Loading } from '@/common/Loader' -import client, { ClientSession } from '@/lib/client' +import Link from 'next/link' +import { ComponentProps, ReactElement, cloneElement, useState } from 'react' + +import { ErrorInfo } from '@/common/ErrorInfo' import { Checkbox, Input } from '@/common/forms' +import { Loading } from '@/common/Loader' import { + OzoneConfig, OzoneConfigFull, getServiceUrlFromDoc, withDocAndMeta, } from '@/lib/client-config' -import { ErrorInfo } from '@/common/ErrorInfo' -import { useSession } from '@/lib/useSession' +import { Agent } from '@atproto/api' +import { useAuthContext, useAuthDid, useAuthIdentifier } from './AuthContext' +import { ConfigurationState } from './ConfigurationContext' + +export type ConfigurationFlowProps = { + state: ConfigurationState + config: OzoneConfig | undefined + error: unknown + labelerAgent: Agent | undefined + skipRecordCreation: () => void | Promise + createRecord: () => void | Promise + reconfigure: () => void | Promise +} -export function ConfigurationFlow() { - const session = useSession() +export function ConfigurationFlow({ + state, + config, + error, + labelerAgent, + skipRecordCreation, + createRecord, + reconfigure, +}: ConfigurationFlowProps) { + const { signOut } = useAuthContext() + + const authDid = useAuthDid() + const authIdentifier = useAuthIdentifier() - if (!session) { + if (state === ConfigurationState.Ready) { + return + } + + if (state === ConfigurationState.Unauthorized) { return ( <> @@ -31,7 +60,7 @@ export function ConfigurationFlow() { @@ -39,21 +68,47 @@ export function ConfigurationFlow() { ) } - const { config } = session + if (state === ConfigurationState.Pending) { + if (error != null) { + return ( + <> + + We encountered an error loading the service configuration: +
+ {error instanceof Error ? error.message : String(error)} +
+ + + ) + } + + return + } + + if (!config || !labelerAgent) { + // Should never happen. Required for type safety. + throw new Error('Invalid state: configuration is missing') + } if (config.needs.key || config.needs.service) { - if (session.did !== session.config.did) { + if (authDid !== config.did) { return ( <> - {`You're`} logged in as {session.handle}. Please login as{' '} - {session.config.handle || 'your Ozone service account'} in order to + {`You're`} logged in as {authIdentifier}. Please login as{' '} + {config.handle || 'your Ozone service account'} in order to configure Ozone. @@ -72,7 +127,7 @@ export function ConfigurationFlow() { @@ -84,13 +139,13 @@ export function ConfigurationFlow() { <> We could not find identity information for the account{' '} - {session.handle}. Are you sure this account has an identity + {authIdentifier}. Are you sure this account has an identity on the network? @@ -108,7 +163,7 @@ export function ConfigurationFlow() { @@ -117,15 +172,8 @@ export function ConfigurationFlow() { } return ( { - await client.reconfigure() - // Now that the labeler's DID document should reflect a service URL - // (i.e. config.needs.service is now false) we are able to make authed - // requests to ozone, so can now fetch server config. - await client.refetchServerConfig() - }} + onIdentityUpdated={reconfigure} /> ) } @@ -167,35 +215,37 @@ export function ConfigurationFlow() { if (config.needs.record) { return ( { - client.reconfigure({ skipRecord: skip }) - }} + config={config} + onSkip={skipRecordCreation} + onCreate={createRecord} /> ) } - return + return } function IdentityConfigurationFlow({ config, - onComplete, + onIdentityUpdated, }: { config: OzoneConfigFull - onComplete: () => void + onIdentityUpdated: () => unknown }) { const [token, setToken] = useState('') + const { pdsAgent, signOut } = useAuthContext() + const requestPlcOperationSignature = useMutation({ + mutationKey: [pdsAgent.assertDid, config.did], mutationFn: async () => { - await client.api.com.atproto.identity.requestPlcOperationSignature() + await pdsAgent.com.atproto.identity.requestPlcOperationSignature() }, }) const submitPlcOperation = useMutation({ + mutationKey: [pdsAgent.assertDid, config.did, config.updatedAt], mutationFn: async () => { - await updatePlcIdentity(token, config) - onComplete() + await updatePlcIdentity(pdsAgent, token, config) + await onIdentityUpdated() }, }) return ( @@ -206,7 +256,7 @@ function IdentityConfigurationFlow({ setup!

- We will be associating your service running at {config.meta.url}{' '} + We will be associating your service running at {config.meta?.url}{' '} with your moderation account {config.handle}. It is highly{' '} recommended not to use a personal account for this.

@@ -222,7 +272,7 @@ function IdentityConfigurationFlow({ disabled={requestPlcOperationSignature.isLoading} className="w-full mr-2" icon={} - onClick={() => client.signout()} + onClick={signOut} > Cancel @@ -276,7 +326,7 @@ function IdentityConfigurationFlow({ } className="w-full mr-2" icon={} - onClick={() => client.signout()} + onClick={signOut} > Cancel @@ -300,27 +350,23 @@ function IdentityConfigurationFlow({ } function RecordConfigurationFlow({ - session, - onComplete, + config, + onSkip, + onCreate, }: { - session: ClientSession - onComplete: (skip: boolean) => void + config: OzoneConfig + onSkip: () => void | Promise + onCreate: () => void | Promise }) { - const { config } = session const [checked, setChecked] = useState(false) + const authDid = useAuthDid() + + const identifier = useAuthIdentifier() + const putServiceRecord = useMutation({ - mutationFn: async () => { - await client.api.com.atproto.repo.putRecord({ - repo: config.did, - collection: 'app.bsky.labeler.service', - rkey: 'self', - record: { - createdAt: new Date().toISOString(), - policies: { labelValues: [] }, - }, - }) - }, + mutationFn: async () => onCreate(), }) + return (

@@ -340,11 +386,11 @@ function RecordConfigurationFlow({ You may skip this step and come back to it next time you login. )} - {session.did !== config.did && ( + {authDid !== config.did && ( - {`You're`} logged in as {session.handle}. Please login as{' '} - {session.config.handle || 'your Ozone service account'} in order to - configure Ozone. + {`You're`} logged in as {identifier}. Please login as{' '} + {config.handle || 'your Ozone service account'} in order to configure + Ozone.

You may skip this step and come back to it next time you login. @@ -361,9 +407,7 @@ function RecordConfigurationFlow({ disabled={putServiceRecord.isLoading || putServiceRecord.isSuccess} className="w-full mr-2" icon={} - onClick={() => { - onComplete(true) - }} + onClick={onSkip} > Skip @@ -371,16 +415,13 @@ function RecordConfigurationFlow({ disabled={ putServiceRecord.isLoading || putServiceRecord.isSuccess || - session.did !== config.did || + authDid !== config.did || config.needs.pds || !checked } className="w-full ml-2" icon={} - onClick={async () => { - await putServiceRecord.mutateAsync() - onComplete(false) - }} + onClick={() => putServiceRecord.mutateAsync()} > Submit @@ -430,7 +471,11 @@ function Button({ ) } -async function updatePlcIdentity(token: string, config: OzoneConfigFull) { +async function updatePlcIdentity( + client: Agent, + token: string, + config: OzoneConfigFull, +) { const services = config.needs.service ? { ...config.doc.services } : undefined if (services) { services['atproto_labeler'] = { @@ -444,18 +489,17 @@ async function updatePlcIdentity(token: string, config: OzoneConfigFull) { if (verificationMethods) { verificationMethods['atproto_label'] = config.meta.publicKey } - const { data: signed } = - await client.api.com.atproto.identity.signPlcOperation({ - token, - verificationMethods, - services, - }) - await client.api.com.atproto.identity.submitPlcOperation({ + const { data: signed } = await client.com.atproto.identity.signPlcOperation({ + token, + verificationMethods, + services, + }) + await client.com.atproto.identity.submitPlcOperation({ operation: signed.operation, }) if (config.handle) { // @NOTE temp hack to push an identity op through - await client.api.com.atproto.identity.updateHandle({ + await client.com.atproto.identity.updateHandle({ handle: config.handle, }) } diff --git a/components/shell/ExternalLabelersContext.tsx b/components/shell/ExternalLabelersContext.tsx new file mode 100644 index 00000000..360747c4 --- /dev/null +++ b/components/shell/ExternalLabelersContext.tsx @@ -0,0 +1,95 @@ +import { + createContext, + ReactNode, + useCallback, + useContext, + useEffect, + useMemo, + useRef, +} from 'react' + +import { unique } from '@/lib/util' +import { useQueryClient } from '@tanstack/react-query' +import { useLocalStorage } from 'react-use' +import { useConfigurationContext } from './ConfigurationContext' + +const KEY = 'external_labeler_value' + +export type ExternalLabelers = string[] +export type ExternalLabelersData = [ + labelers: ExternalLabelers, + setLabelers: (value: ExternalLabelers) => void, +] + +export const ExternalLabelersContext = + createContext(null) + +export const ExternalLabelersProvider = ({ + children, +}: { + children: ReactNode +}) => { + const { config, labelerAgent } = useConfigurationContext() + const queryClient = useQueryClient() + + const [state = [], setState] = useLocalStorage(KEY, [], { + raw: false, + serializer: JSON.stringify, + deserializer: (value) => { + try { + const parsed = JSON.parse(value) + if (Array.isArray(parsed)) return unique(parsed) + // Migrate legacy data + return Object.keys(parsed) + } catch { + return [] + } + }, + }) + + const externalLabelers = useMemo( + () => unique(state.filter((did) => did !== config.did)), + [config.did, state], + ) + const setExternalLabelers = useCallback( + (value: ExternalLabelers): void => { + setState(unique(value).filter((did) => did !== config.did)) + }, + [setState, config.did], + ) + + // Keep the labelers header up-to-date with the external labelers + useEffect(() => { + labelerAgent.configureLabelers([config.did, ...externalLabelers]) + }, [labelerAgent, config.did, externalLabelers]) + + // Invalidate all queries whenever the external labelers (really) change + const externalLabelersRef = useRef(externalLabelers) + useEffect(() => { + if (externalLabelersRef.current !== externalLabelers) { + externalLabelersRef.current = externalLabelers + queryClient.invalidateQueries() + } + }, [queryClient, externalLabelers]) + + // Expose external labelers state as context value + const value = useMemo( + () => [externalLabelers, setExternalLabelers], + [externalLabelers, setExternalLabelers], + ) + + return ( + + {children} + + ) +} + +export function useExternalLabelers() { + const context = useContext(ExternalLabelersContext) + if (context) return context + + throw new Error( + 'useExternalLabelersContext must be used within an ExternalLabelersProvider', + ) +} diff --git a/components/shell/LoginModal.tsx b/components/shell/LoginModal.tsx deleted file mode 100644 index 111fd420..00000000 --- a/components/shell/LoginModal.tsx +++ /dev/null @@ -1,259 +0,0 @@ -'use client' -import Image from 'next/image' -import { FormEvent, useState, useEffect, useContext, createRef } from 'react' -import { LockClosedIcon, XCircleIcon } from '@heroicons/react/20/solid' -import { AuthChangeContext, AuthContext } from './AuthContext' -import { AuthState } from '@/lib/types' -import Client from '@/lib/client' -import { ConfigurationFlow } from './ConfigurationFlow' -import { ErrorInfo } from '@/common/ErrorInfo' -import { Alert } from '@/common/Alert' -import { ComAtprotoServerCreateSession } from '@atproto/api' - -export function LoginModal() { - const { isValidatingAuth, isLoggedIn, authState } = useContext(AuthContext) - const setAuthContextData = useContext(AuthChangeContext) - const [error, setError] = useState('') - const [service, setService] = useState('https://bsky.social') - const [handle, setHandle] = useState('') - const [password, setPassword] = useState('') - const [authFactor, setAuthFactor] = useState<{ - token: string - isInvalid: boolean - isNeeded: boolean - }>({ - token: '', - isNeeded: false, - isInvalid: false, - }) - const handleRef = createRef() - - const submitButtonClassNames = `group relative flex w-full justify-center rounded-md border border-transparent py-2 px-4 text-sm font-medium text-white focus:outline-none focus:ring-2 focus:ring-rose-500 dark:focus:ring-slate-500 focus:ring-offset-2 ${ - isValidatingAuth - ? 'bg-gray-500 hover:bg-gray-600' - : 'bg-rose-600 dark:bg-teal-600 hover:bg-rose-700 dark:hover:bg-teal-700' - }` - const submitButtonIconClassNames = `h-5 w-5 ${ - isValidatingAuth - ? 'text-gray-800 group-hover:text-gray-700' - : 'text-rose-500 dark:text-gray-50 group-hover:text-rose-400 dark:group-hover:text-gray-100' - }` - - useEffect(() => { - if (!Client.hasSetup) { - Client.setup() - .then((authState) => setAuthContextData(authState)) - .catch(() => setAuthContextData(AuthState.LoggedOut)) - } - }, []) - - useEffect(() => { - const title = `Ozone - Authenticate` - if (!isLoggedIn) { - document.title = title - } - }, [isLoggedIn]) - - const onSubmit = async (e: FormEvent) => { - e.preventDefault() - e.stopPropagation() - try { - setAuthContextData(AuthState.Validating) - const authState = await Client.signin( - service, - handle, - password, - authFactor.token.trim(), - ) - setAuthContextData(authState) - } catch (e: any) { - console.error(e) - const errMsg = e.toString() - if ( - e instanceof ComAtprotoServerCreateSession.AuthFactorTokenRequiredError - ) { - setAuthFactor({ ...authFactor, isNeeded: true }) - } else if (errMsg.includes('Token is invalid')) { - setAuthFactor({ ...authFactor, isInvalid: true }) - } else { - setError(errMsg) - } - } - } - - if (isLoggedIn) { - return <> - } - return ( -

-
-
-
- Ozone - Bluesky Admin -

- Bluesky Admin Tools -

-

- {authState === AuthState.LoggedInUnconfigured && ( - <>Configure your Ozone service - )} - {authState !== AuthState.LoggedInUnconfigured && ( - <>Sign into your account - )} -

-
- {authState === AuthState.LoggedInUnconfigured && ( - - )} - {authState !== AuthState.LoggedInUnconfigured && ( -
- -
-
- - setService(e.target.value)} - /> - - -
-
- - setHandle(e.target.value)} - /> -
-
- - setPassword(e.target.value)} - /> -
- - {/* When user fills in the token and hits submit again, the AuthState value changes to Validating so the input field goes away which is a bit odd */} - {authFactor.isNeeded && ( -
- - - setAuthFactor({ ...authFactor, token: e.target.value }) - } - /> -
- )} -
- - {authFactor.isNeeded && ( - - Check your email for a confirmation code and enter it here - or{' '} - - - } - /> - )} - - {error ? {error} : undefined} - -
- -
- - )} -
-
-
- ) -} diff --git a/components/shell/ProfileMenu.tsx b/components/shell/ProfileMenu.tsx index dfa02297..ac8556b8 100644 --- a/components/shell/ProfileMenu.tsx +++ b/components/shell/ProfileMenu.tsx @@ -1,64 +1,23 @@ 'use client' -import { - useEffect, - useState, - SyntheticEvent, - Fragment, - useContext, -} from 'react' import { Menu, Transition } from '@headlessui/react' +import { Fragment, SyntheticEvent } from 'react' + import { classNames } from '@/lib/util' -import Client from '@/lib/client' -import { AuthChangeContext, AuthContext, AuthState } from './AuthContext' +import { + useAuthContext, + useAuthIdentifier, + useAuthProfile, +} from './AuthContext' export function ProfileMenu() { - const { isLoggedIn } = useContext(AuthContext) - const setAuthContextData = useContext(AuthChangeContext) - const [handle, setHandle] = useState('') - const [avatar, setAvatar] = useState('') - - useEffect(() => { - const onClientChange = () => setAuthContextData(Client.authState) - Client.addEventListener('change', onClientChange) - return () => Client.removeEventListener('change', onClientChange) - }, []) + const { pdsAgent, signOut } = useAuthContext() - useEffect(() => { - let aborted = false - if (Client.authState !== AuthState.LoggedIn) { - localStorage.cachedProfileHandle = '' - localStorage.cachedProfileAvatar = '' - setHandle('') - setAvatar('') - return - } - if ( - Client.session.handle === localStorage.cachedProfileHandle && - localStorage.cachedProfileAvatar - ) { - setHandle(localStorage.cachedProfileHandle) - setAvatar(localStorage.cachedProfileAvatar) - return - } - Client.api.app.bsky.actor.getProfile({ actor: Client.session.did }).then( - (res) => { - localStorage.cachedProfileHandle = res.data.handle - localStorage.cachedProfileAvatar = res.data.avatar || '' - setHandle(res.data.handle) - setAvatar(res.data.avatar || '') - }, - (err) => { - console.error('Failed to fetch user profile', err) - }, - ) - return () => { - aborted = true - } - }, [isLoggedIn]) + const identifier = useAuthIdentifier() + const avatar = useAuthProfile()?.avatar - const onClickSignout = (e: SyntheticEvent) => { + const onClickSignout = async (e: SyntheticEvent) => { e.preventDefault() - Client.signout() + await signOut() window.location.reload() } @@ -70,7 +29,7 @@ export function ProfileMenu() { Open user menu - {handle || ''} + {identifier || ''} ( + undefined, +) + +/** + * Provides a `QueryClient` that is meant to be used for queries that + * do not depend on the current user. + */ +export function GlobalQueryClientProvider({ + children, +}: { + children: ReactNode +}) { + const [queryClient] = useState(createQueryClient) + return ( + + {children} + + ) +} + +/** + * Provides a `QueryClient` that is reset when the account changes. + */ +export function DefaultQueryClientProvider({ + children, +}: { + children: ReactNode +}) { + const [queryClient] = useState(createQueryClient) + const accountDid = useAuthDid() + + // Keep a reference to avoid double clear in strict mode + const accountDidRef = useRef(accountDid) + + useEffect(() => { + if (accountDidRef.current !== accountDid) { + accountDidRef.current = accountDid + queryClient.clear() + } + }, [queryClient, accountDid]) + + return ( + {children} + ) +} + +function createQueryClient() { + return new QueryClient() +} diff --git a/components/shell/Shell.tsx b/components/shell/Shell.tsx index f1796ed9..752313d3 100644 --- a/components/shell/Shell.tsx +++ b/components/shell/Shell.tsx @@ -4,10 +4,9 @@ import { MagnifyingGlassIcon } from '@heroicons/react/20/solid' import { SidebarNav } from './SidebarNav' import { MobileMenuProvider, MobileMenu, MobileMenuBtn } from './MobileMenu' import { ProfileMenu } from './ProfileMenu' -import { LoginModal } from './LoginModal' import { useCommandPaletteAsyncSearch } from './CommandPalette/useAsyncSearch' -import { useFluentReportSearch } from '@/reports/useFluentReportSearch' +import { useFluentReportSearchUpdate } from '@/reports/useFluentReportSearch' import { useSyncedState } from '@/lib/useSyncedState' import Image from 'next/image' import { Suspense } from 'react' @@ -19,7 +18,6 @@ export function Shell({ children }: React.PropsWithChildren) { return ( -
{/* Narrow sidebar */}
@@ -94,7 +92,7 @@ export function Shell({ children }: React.PropsWithChildren) { function SearchInput() { const params = useSearchParams() - const { updateParams } = useFluentReportSearch() + const updateParams = useFluentReportSearchUpdate() const [termInput, setTermInput] = useSyncedState(params.get('term') ?? '') return ( diff --git a/components/shell/auth/credential/CredentialSignInForm.tsx b/components/shell/auth/credential/CredentialSignInForm.tsx new file mode 100644 index 00000000..826b4375 --- /dev/null +++ b/components/shell/auth/credential/CredentialSignInForm.tsx @@ -0,0 +1,228 @@ +import { ComAtprotoServerCreateSession } from '@atproto/api' +import { LockClosedIcon } from '@heroicons/react/20/solid' +import { createRef, FormEvent, useCallback, useState } from 'react' + +import { Alert } from '@/common/Alert' +import { ErrorInfo } from '@/common/ErrorInfo' + +export type CredentialSignIn = (input: { + identifier: string + password: string + authFactorToken?: string + service: string +}) => unknown + +/** + * @returns Nice tailwind css form asking to enter either a handle or the host + * to use to login. + */ +export function CredentialSignInForm({ + signIn, + ...props +}: { + signIn: CredentialSignIn +} & Omit, 'onSubmit'>) { + const [error, setError] = useState(null) + const [isValidatingAuth, setIsValidatingAuth] = useState(false) + + const [handle, setHandle] = useState('') + const [password, setPassword] = useState('') + const [service, setService] = useState('http://localhost:2583') + const [authFactor, setAuthFactor] = useState<{ + token: string + isInvalid: boolean + isNeeded: boolean + }>({ + token: '', + isNeeded: false, + isInvalid: false, + }) + + const handleRef = createRef() + + const onSubmit = useCallback( + async (e: FormEvent) => { + e.preventDefault() + e.stopPropagation() + + if (isValidatingAuth) return + + setIsValidatingAuth(true) + + try { + await signIn({ + identifier: handle, + password, + service, + authFactorToken: authFactor.isNeeded ? authFactor.token : undefined, + }) + } catch (err) { + const errMsg = (err as Error).toString() + if ( + err instanceof + ComAtprotoServerCreateSession.AuthFactorTokenRequiredError + ) { + setAuthFactor({ ...authFactor, isNeeded: true }) + } else if (errMsg.includes('Token is invalid')) { + setAuthFactor({ ...authFactor, isInvalid: true }) + } else { + setError(errMsg) + } + } finally { + setIsValidatingAuth(false) + } + }, + [authFactor, isValidatingAuth, handle, password, service, signIn], + ) + + const submitButtonClassNames = `group relative flex w-full justify-center rounded-md border border-transparent py-2 px-4 text-sm font-medium text-white focus:outline-none focus:ring-2 focus:ring-rose-500 dark:focus:ring-slate-500 focus:ring-offset-2 ${ + isValidatingAuth + ? 'bg-gray-500 hover:bg-gray-600' + : 'bg-rose-600 dark:bg-teal-600 hover:bg-rose-700 dark:hover:bg-teal-700' + }` + const submitButtonIconClassNames = `h-5 w-5 ${ + isValidatingAuth + ? 'text-gray-800 group-hover:text-gray-700' + : 'text-rose-500 dark:text-gray-50 group-hover:text-rose-400 dark:group-hover:text-gray-100' + }` + + return ( +
+ +
+
+ + setService(e.target.value)} + /> + + +
+
+ + setHandle(e.target.value)} + /> +
+
+ + setPassword(e.target.value)} + /> +
+ + {/* When user fills in the token and hits submit again, the AuthState value changes to Validating so the input field goes away which is a bit odd */} + {authFactor.isNeeded && ( +
+ + + setAuthFactor({ ...authFactor, token: e.target.value }) + } + /> +
+ )} +
+ + {authFactor.isNeeded && ( + + Check your email for a confirmation code and enter it here or{' '} + + + } + /> + )} + + {error ? {error} : undefined} + +
+ +
+ + ) +} diff --git a/components/shell/auth/credential/useCredential.ts b/components/shell/auth/credential/useCredential.ts new file mode 100644 index 00000000..563cf181 --- /dev/null +++ b/components/shell/auth/credential/useCredential.ts @@ -0,0 +1,81 @@ +import { AtpSessionData, CredentialSession } from '@atproto/api' +import { useCallback, useEffect, useMemo, useState } from 'react' + +type Session = AtpSessionData & { service: string } + +export function useCredential() { + const createSession = useCallback((service: string) => { + const persistSession = (type, session) => { + if (session) { + saveSession({ ...session, service }) + } else { + setSession(null) + deleteSession() + } + } + return new CredentialSession(new URL(service), undefined, persistSession) + }, []) + + const [session, setSession] = useState(null) + + useEffect(() => { + const prev = loadSession() + if (!prev) return + + const session = createSession(prev.service) + session.resumeSession(prev).then(() => setSession((s) => s || session)) + }, []) + + const signIn = useCallback( + async ({ + identifier, + password, + authFactorToken, + service, + }: { + identifier: string + password: string + authFactorToken?: string + service: string + }) => { + const session = createSession(service) + await session.login({ identifier, password, authFactorToken }) + setSession(session) + }, + [createSession], + ) + + return useMemo( + () => ({ session, signIn, signOut: () => session?.logout() }), + [session, signIn], + ) +} + +const SESSION_KEY = 'ozone_session' + +function loadSession(): Session | undefined { + try { + const str = localStorage.getItem(SESSION_KEY) + const obj: unknown = str ? JSON.parse(str) : undefined + if ( + obj?.['service'] && + obj?.['refreshJwt'] && + obj?.['accessJwt'] && + obj?.['handle'] && + obj?.['did'] + ) { + return obj as Session + } + return undefined + } catch (e) { + return undefined + } +} + +function saveSession(session: Session) { + localStorage.setItem(SESSION_KEY, JSON.stringify(session)) +} + +function deleteSession() { + localStorage.removeItem(SESSION_KEY) +} diff --git a/components/shell/auth/oauth/OAuthSignInForm.tsx b/components/shell/auth/oauth/OAuthSignInForm.tsx new file mode 100644 index 00000000..82dfa47e --- /dev/null +++ b/components/shell/auth/oauth/OAuthSignInForm.tsx @@ -0,0 +1,99 @@ +import type { AuthorizeOptions } from '@atproto/oauth-client-browser' +import { LockClosedIcon } from '@heroicons/react/20/solid' +import { FormEvent, useCallback, useState } from 'react' + +import { ErrorInfo } from '@/common/ErrorInfo' +import { OAuthSignIn } from './useOAuth' + +export type { OAuthSignIn } + +/** + * @returns Nice tailwind css form asking to enter either a handle or the host + * to use to login. + */ +export function OAuthSignInForm({ + signIn, + ...props +}: { + signIn: OAuthSignIn +} & Omit, 'onSubmit'>) { + const [value, setValue] = useState('') + const [error, setError] = useState(null) + const [loading, setLoading] = useState(false) + + const onSubmit = useCallback( + async (e: FormEvent) => { + e.preventDefault() + e.stopPropagation() + + if (loading) return + + setError(null) + setLoading(true) + + try { + await signIn(value) + } catch (err) { + setError(err?.['message'] || String(err)) + } finally { + setLoading(false) + } + }, + [loading, value, signIn], + ) + + const submitButtonClassNames = `group relative flex w-full justify-center rounded-md border border-transparent py-2 px-4 text-sm font-medium text-white focus:outline-none focus:ring-2 focus:ring-rose-500 dark:focus:ring-slate-500 focus:ring-offset-2 ${ + loading + ? 'bg-gray-500 hover:bg-gray-600' + : 'bg-rose-600 dark:bg-teal-600 hover:bg-rose-700 dark:hover:bg-teal-700' + }` + const submitButtonIconClassNames = `h-5 w-5 ${ + loading + ? 'text-gray-800 group-hover:text-gray-700' + : 'text-rose-500 dark:text-gray-50 group-hover:text-rose-400 dark:group-hover:text-gray-100' + }` + + return ( +
+
+
+ + { + setError(null) + setValue(e.target.value) + }} + /> +
+
+ + {error != null ? {error} : undefined} + +
+ +
+
+ ) +} diff --git a/components/shell/auth/oauth/useOAuth.ts b/components/shell/auth/oauth/useOAuth.ts new file mode 100644 index 00000000..ead2ed1f --- /dev/null +++ b/components/shell/auth/oauth/useOAuth.ts @@ -0,0 +1,222 @@ +'use client' + +import { + // Only type imports are allowed here to avoid SSR issues + type BrowserOAuthClient, + type BrowserOAuthClientLoadOptions, + type OAuthSession, +} from '@atproto/oauth-client-browser' +import { useEffect, useMemo, useRef, useState } from 'react' + +import { useCallbackRef } from '@/lib/useCallbackRef' +import { useValueRef } from '@/lib/useValueRef' +import { useSignaledEffect } from '@/lib/useSignaledEffect' + +export type OAuthSignIn = (input: string) => unknown + +export type OnRestored = (session: OAuthSession | null) => void +export type OnSignedIn = (session: OAuthSession, state: null | string) => void +export type OnSignedOut = () => void + +type ClientOptions = Partial< + Pick< + BrowserOAuthClientLoadOptions, + 'clientId' | 'handleResolver' | 'responseMode' | 'plcDirectoryUrl' | 'fetch' + > +> + +function useOAuthClient(options: ClientOptions) { + const { clientId, handleResolver, responseMode, plcDirectoryUrl } = options + + const [client, setClient] = useState(null) + const fetch = useCallbackRef(options.fetch || globalThis.fetch) + + useSignaledEffect( + (signal) => { + if (clientId && handleResolver) { + // Clear current value (if any) + setClient(null) + + // "oauth-client-browser" is not compatible with SSR, so we load it + // dynamically from an effect. Only type imports are allowed at the top. + void import('@atproto/oauth-client-browser').then(async (mod) => { + if (signal.aborted) return + + const client = await mod.BrowserOAuthClient.load({ + clientId, + handleResolver, + responseMode, + plcDirectoryUrl, + fetch, + signal, + }) + + if (signal.aborted) { + client.dispose() + } else { + signal.addEventListener('abort', () => client.dispose(), { + once: true, + }) + setClient(client) + } + }) + } else { + setClient(null) + } + }, + [clientId, handleResolver, responseMode, plcDirectoryUrl, fetch], + ) + + return client +} + +export type UseOAuthOptions = ClientOptions & { + onRestored?: OnRestored + onSignedIn?: OnSignedIn + onSignedOut?: OnSignedOut + + state?: string + scope?: string +} + +export function useOAuth(options: UseOAuthOptions) { + const onRestored = useCallbackRef(options.onRestored) + const onSignedIn = useCallbackRef(options.onSignedIn) + const onSignedOut = useCallbackRef(options.onSignedOut) + + const clientForInit = useOAuthClient(options) + + const scopeRef = useValueRef(options.scope) + const stateRef = useValueRef(options.state) + + const [session, setSession] = useState(null) + const [client, setClient] = useState(null) + const [isInitializing, setIsInitializing] = useState(true) + const [isLoginPopup, setIsLoginPopup] = useState(false) + + const clientForInitRef = useRef() + useEffect(() => { + // In strict mode, we don't want to re-init() the client if it's the same + if (clientForInitRef.current === clientForInit) return + clientForInitRef.current = clientForInit + + setSession(null) + setClient(null) + setIsLoginPopup(false) + setIsInitializing(clientForInit != null) + + clientForInit + ?.init() + .then( + async (r) => { + if (clientForInitRef.current !== clientForInit) return + + setClient(clientForInit) + if (r) { + setSession(r.session) + + if ('state' in r) { + await onSignedIn(r.session, r.state) + } else { + await onRestored(r.session) + } + } else { + await onRestored(null) + } + }, + async (err) => { + if (clientForInitRef.current !== clientForInit) return + const { LoginContinuedInParentWindowError } = await import( + '@atproto/oauth-client-browser' + ) + if (err instanceof LoginContinuedInParentWindowError) { + setIsLoginPopup(true) + return + } + + setClient(clientForInit) + await onRestored(null) + + console.error('Failed to init:', err) + }, + ) + .finally(() => { + if (clientForInitRef.current !== clientForInit) return + + setIsInitializing(false) + }) + }, [clientForInit, onSignedIn, onRestored]) + + useEffect(() => { + if (!client) return + + const controller = new AbortController() + const { signal } = controller + + client.addEventListener( + 'updated', + ({ detail: { sub } }) => { + if (!session || session.did !== sub) { + setSession(null) + client.restore(sub, false).then((session) => { + if (!signal.aborted) setSession(session) + }) + } + }, + { signal }, + ) + + if (session) { + client.addEventListener( + 'deleted', + ({ detail: { sub } }) => { + if (session.did === sub) { + setSession(null) + void onSignedOut() + } + }, + { signal }, + ) + } + + return () => { + controller.abort() + } + }, [client, session, onSignedOut]) + + // Memoize the return value to avoid re-renders in consumers + return useMemo( + () => ({ + isInitializing, + isInitialized: client != null, + isLoginPopup, + + client, + session, + + signIn: client + ? async (input) => { + const state = stateRef.current + const scope = scopeRef.current + const session = await client.signIn(input, { scope, state }) + setSession(session) + await onSignedIn(session, null) + return session + } + : () => { + throw new Error('Client not initialized') + }, + signOut: () => session?.signOut(), + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + isInitializing, + isLoginPopup, + session, + client, + // onSignedIn + // stateRef + // scopeRef + ], + ) +} diff --git a/components/subject/useSubjectStatus.tsx b/components/subject/useSubjectStatus.tsx index 4bae5d04..0f52c8f6 100644 --- a/components/subject/useSubjectStatus.tsx +++ b/components/subject/useSubjectStatus.tsx @@ -1,18 +1,21 @@ import { useQuery } from '@tanstack/react-query' -import client from '@/lib/client' import { ToolsOzoneModerationDefs } from '@atproto/api' +import { useLabelerAgent } from '@/shell/ConfigurationContext' export const useSubjectStatus = ({ subject }: { subject: string | null }) => { + const labelerAgent = useLabelerAgent() return useQuery({ queryKey: ['moderationStatus', { subject }], cacheTime: 60 * 1000, staleTime: 60 * 1000, queryFn: async () => { if (!subject) return null - const { data } = await client.api.tools.ozone.moderation.queryStatuses( - { subject, includeMuted: true, limit: 1 }, - { headers: client.proxyHeaders() }, - ) + const { data } = + await labelerAgent.api.tools.ozone.moderation.queryStatuses({ + subject, + includeMuted: true, + limit: 1, + }) return data }, }) @@ -27,6 +30,7 @@ export type StatusBySubject = Record< // but even with a bulk fetcher, there would probably be some kind of restriction on how many subjects // we can fetch at once, so we would need some kind of queue/list fetcher anyways export const useSubjectStatuses = ({ subjects }: { subjects: string[] }) => { + const labelerAgent = useLabelerAgent() return useQuery({ queryKey: ['moderationStatuses', subjects], cacheTime: 60 * 1000, @@ -35,11 +39,8 @@ export const useSubjectStatuses = ({ subjects }: { subjects: string[] }) => { const statusBySubject: StatusBySubject = {} await Promise.allSettled( subjects.map((subject) => { - return client.api.tools.ozone.moderation - .queryStatuses( - { subject, includeMuted: true, limit: 1 }, - { headers: client.proxyHeaders() }, - ) + return labelerAgent.api.tools.ozone.moderation + .queryStatuses({ subject, includeMuted: true, limit: 1 }) .then(({ data }) => { if (data?.subjectStatuses[0]) { statusBySubject[subject] = data.subjectStatuses[0] diff --git a/components/team/MemberEditor.tsx b/components/team/MemberEditor.tsx index b9ad45e2..a7d3497a 100644 --- a/components/team/MemberEditor.tsx +++ b/components/team/MemberEditor.tsx @@ -6,10 +6,10 @@ import { Alert } from '@/common/Alert' import { ActionButton } from '@/common/buttons' import { Card } from '@/common/Card' import { Checkbox, FormLabel, Input, Select } from '@/common/forms' -import client from '@/lib/client' import { getDidFromHandle } from '@/lib/identity' -import { queryClient } from 'components/QueryClient' import { MemberRoleNames } from './Role' +import { useLabelerAgent } from '@/shell/ConfigurationContext' +import { useQueryClient } from '@tanstack/react-query' const getSubmitButtonText = ( member: ToolsOzoneTeamDefs.Member | null, @@ -28,6 +28,9 @@ const useMemberEditor = ({ isNewMember: boolean onSuccess: () => void }) => { + const labelerAgent = useLabelerAgent() + const queryClient = useQueryClient() + const [submission, setSubmission] = useState<{ isSubmitting: boolean error: string @@ -62,23 +65,17 @@ const useMemberEditor = ({ // anything in the UI so we don't need to type it let request: Promise if (isNewMember) { - request = client.api.tools.ozone.team.addMember( - { - did, - role, - }, - { encoding: 'application/json', headers: client.proxyHeaders() }, - ) + request = labelerAgent.api.tools.ozone.team.addMember({ + did, + role, + }) } else { const disabled = formData.get('disabled') === 'true' - request = client.api.tools.ozone.team.updateMember( - { - did, - role, - disabled, - }, - { encoding: 'application/json', headers: client.proxyHeaders() }, - ) + request = labelerAgent.api.tools.ozone.team.updateMember({ + did, + role, + disabled, + }) } await toast.promise(request, { diff --git a/components/team/MemberList.tsx b/components/team/MemberList.tsx index 0977468d..63cd4e23 100644 --- a/components/team/MemberList.tsx +++ b/components/team/MemberList.tsx @@ -2,7 +2,6 @@ import { ActionButton } from '@/common/buttons' import { Card } from '@/common/Card' import { LabelChip } from '@/common/labels' import { useActionPanelLink } from '@/common/useActionPanelLink' -import { useSession } from '@/lib/useSession' import { ToolsOzoneTeamDefs } from '@atproto/api' import { PencilIcon } from '@heroicons/react/20/solid' import Link from 'next/link' @@ -10,6 +9,7 @@ import { RoleTag } from './Role' import { SOCIAL_APP_URL } from '@/lib/constants' import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/solid' import { LoadMoreButton } from '@/common/LoadMoreButton' +import { useAuthDid } from '@/shell/AuthContext' export function MemberList({ members, @@ -27,7 +27,7 @@ export function MemberList({ onEdit: (member: ToolsOzoneTeamDefs.Member) => void }) { const createActionPanelLink = useActionPanelLink() - const session = useSession() + const authDid = useAuthDid() return ( <> @@ -37,7 +37,7 @@ export function MemberList({
{!members?.length &&

No members found.

} {members?.map((member, i) => { - const isCurrentMember = session?.did === member.did + const isCurrentMember = authDid === member.did const lastItem = i === members.length - 1 return (
- useInfiniteQuery({ +export const useMemberList = () => { + const labelerAgent = useLabelerAgent() + return useInfiniteQuery({ queryKey: ['memberList'], queryFn: async ({ pageParam }) => { - const { data } = await client.api.tools.ozone.team.listMembers( - { - limit: 25, - cursor: pageParam, - }, - { headers: client.proxyHeaders() }, - ) + const { data } = await labelerAgent.api.tools.ozone.team.listMembers({ + limit: 25, + cursor: pageParam, + }) return data }, getNextPageParam: (lastPage) => lastPage.cursor, }) +} diff --git a/components/workspace/Panel.tsx b/components/workspace/Panel.tsx index af0acd4c..d0354c09 100644 --- a/components/workspace/Panel.tsx +++ b/components/workspace/Panel.tsx @@ -6,7 +6,6 @@ import { FormEvent, useRef, useState } from 'react' import { ActionPanel } from '@/common/ActionPanel' import { PropsOf } from '@/lib/types' import { FullScreenActionPanel } from '@/common/FullScreenActionPanel' -import { createBreakpoint } from 'react-use' import { CheckCircleIcon } from '@heroicons/react/24/outline' import { MOD_EVENTS } from '@/mod-event/constants' import { Dialog } from '@headlessui/react' @@ -21,15 +20,7 @@ import { useSubjectStatuses } from '@/subject/useSubjectStatus' import { WorkspacePanelActions } from './PanelActions' import { WORKSPACE_FORM_ID } from './constants' import { WorkspacePanelActionForm } from './PanelActionForm' -import clientManager from '@/lib/client' -import { actionSubjects } from '@/mod-event/helpers/emitEvent' - -const useBreakpoint = createBreakpoint({ xs: 340, sm: 640 }) - -const dateFormatter = new Intl.DateTimeFormat('en-US', { - dateStyle: 'medium', - timeStyle: 'short', -}) +import { useActionSubjects } from '@/mod-event/helpers/emitEvent' export function WorkspacePanel(props: PropsOf) { const { onClose, ...others } = props @@ -42,6 +33,7 @@ export function WorkspacePanel(props: PropsOf) { MOD_EVENTS.ACKNOWLEDGE, ) const [showItemCreator, setShowItemCreator] = useState(false) + const actionSubjects = useActionSubjects() const handleSelectAll = () => { const checkboxes = formRef.current?.querySelectorAll( @@ -121,10 +113,7 @@ export function WorkspacePanel(props: PropsOf) { // No need to break if one of the requests fail, continue on with others await actionSubjects( - { - event: coreEvent, - createdBy: clientManager.session.did, - }, + { event: coreEvent }, Array.from(formData.getAll('workspaceItem') as string[]), ) diff --git a/cypress/e2e/auth/main.cy.ts b/cypress/e2e/auth/main.cy.ts index c3b54779..615f1c67 100644 --- a/cypress/e2e/auth/main.cy.ts +++ b/cypress/e2e/auth/main.cy.ts @@ -6,7 +6,7 @@ describe('Authentication', () => { let authFixture beforeEach(() => { - cy.visit('http://localhost:3000') + cy.visit('http://127.0.0.1:3000') cy.fixture('auth.json').then((data) => (authFixture = data)) }) @@ -21,7 +21,7 @@ describe('Authentication', () => { }, }) - cy.get('#service-url').should('have.value', 'https://bsky.social') + cy.get('#service-url').should('have.value', 'http://localhost:2583') cy.get('#account-handle').type('alice.test') cy.get('#password').type('hunter2') cy.get("button[type='submit']").click() diff --git a/cypress/e2e/cmd-palette/main.cy.ts b/cypress/e2e/cmd-palette/main.cy.ts index f60d53a8..048c13d0 100644 --- a/cypress/e2e/cmd-palette/main.cy.ts +++ b/cypress/e2e/cmd-palette/main.cy.ts @@ -1,27 +1,9 @@ /// " -import { mockServerConfigResponse } from '../../support/api' +import { HANDLE_RESOLVER_URL, SERVER_URL } from '../../support/api' describe('Command Palette', () => { - const SERVER_URL = 'https://bsky.social/xrpc' - const PLC_URL = 'https://plc.directory' - - const mockAuthResponse = (response: Record) => - cy.intercept( - 'POST', - `${SERVER_URL}/com.atproto.server.createSession`, - response, - ) - - const mockRepoResponse = (response: Record) => - cy.intercept( - 'GET', - `${SERVER_URL}/tools.ozone.moderation.getRepo*`, - response, - ) - - const mockProfileResponse = (response: Record) => - cy.intercept('GET', `${SERVER_URL}/app.bsky.actor.getProfile*`, response) + let statusesFixture const mockModerationReportsResponse = (response: Record) => cy.intercept( @@ -29,48 +11,14 @@ describe('Command Palette', () => { `${SERVER_URL}/tools.ozone.moderation.queryStatuses*`, response, ) - const mockOzoneMetaResponse = (response: Record) => - cy.intercept('GET', '/.well-known/ozone-metadata.json', response) - const mockOzoneDidDataResponse = (response: Record) => - cy.intercept('GET', `${PLC_URL}/*/data`, response) + const mockResolveHandleResponse = (response: Record) => cy.intercept( 'GET', - `${SERVER_URL}/com.atproto.identity.resolveHandle*`, + `${HANDLE_RESOLVER_URL}/com.atproto.identity.resolveHandle*`, response, ) - const setupLoginMocks = () => { - mockAuthResponse({ - statusCode: 200, - body: authFixture.createSessionResponse, - }) - mockResolveHandleResponse({ - statusCode: 200, - body: authFixture.createResolveHandleResponse, - }) - mockRepoResponse({ - statusCode: 200, - body: authFixture.getRepoResponse, - }) - mockProfileResponse({ - statusCode: 200, - body: authFixture.getProfileResponse, - }) - mockOzoneMetaResponse({ - statusCode: 200, - body: authFixture.ozoneMetaResponse, - }) - mockOzoneDidDataResponse({ - statusCode: 200, - body: authFixture.ozoneDidDataResponse, - }) - mockServerConfigResponse({ - statusCode: 200, - body: authFixture.ozoneServerConfigResponse, - }) - } - const openCommandPalette = (input?: string) => { const comboKey = Cypress.platform === 'darwin' ? '{cmd}k' : '{ctrl}k' cy.get('body').type(comboKey) @@ -85,23 +33,26 @@ describe('Command Palette', () => { 'https://bsky.app/profile/alice.test/post/3kozf56ocx32a' beforeEach(() => { - cy.visit('http://localhost:3000') + cy.visit('http://127.0.0.1:3000') + cy.fixture('statuses.json').then((data) => { + statusesFixture = data + mockModerationReportsResponse(statusesFixture.multiCidLabeledProfile) + }) cy.fixture('auth.json').then((data) => { authFixture = data - - setupLoginMocks() - cy.get('#service-url').should('have.value', 'https://bsky.social') - cy.get('#account-handle').type('alice.test') - cy.get('#password').type('hunter2') - cy.get("button[type='submit']").click() - cy.wait(1000) + cy.login(data) + mockResolveHandleResponse({ + statusCode: 200, + body: data.createResolveHandleResponse, + }) }) }) it('Shows post options from bsky app post url', () => { + cy.wait(500) openCommandPalette(bskyPostUrlWithHandle) cy.get('#kbar-listbox-item-1').contains('Take action on Post').click() - cy.wait(300) + cy.wait(500) cy.location('href').then((href) => { expect(decodeURIComponent(href)).to.include( `quickOpen=at://did:plc:56ud7t6bqdkwblmzwmkcetst/app.bsky.feed.post/3kozf56ocx32a`, @@ -110,9 +61,10 @@ describe('Command Palette', () => { }) it('Shows user options from bsky app post url', () => { + cy.wait(500) openCommandPalette(bskyPostUrlWithHandle) cy.get('#kbar-listbox-item-2').contains('Take action on alice.test').click() - cy.wait(300) + cy.wait(500) cy.location('href').then((href) => { expect(decodeURIComponent(href)).to.include( `quickOpen=did:plc:56ud7t6bqdkwblmzwmkcetst`, diff --git a/cypress/e2e/mod-action/label.cy.ts b/cypress/e2e/mod-action/label.cy.ts index c2d165eb..ba2c13f9 100644 --- a/cypress/e2e/mod-action/label.cy.ts +++ b/cypress/e2e/mod-action/label.cy.ts @@ -12,7 +12,7 @@ describe('Mod Action -> Label', () => { let seedFixture beforeEach(() => { - cy.visit('http://localhost:3000') + cy.visit('http://127.0.0.1:3000') cy.fixture('statuses.json').then((data) => { statusesFixture = data mockModerationReportsResponse(statusesFixture.onlyRepo) diff --git a/cypress/e2e/mod-action/multi-cid-label.cy.ts b/cypress/e2e/mod-action/multi-cid-label.cy.ts index dae3c1ca..7c92bc7f 100644 --- a/cypress/e2e/mod-action/multi-cid-label.cy.ts +++ b/cypress/e2e/mod-action/multi-cid-label.cy.ts @@ -14,7 +14,7 @@ describe('Mod Action -> Label', () => { let seedFixture beforeEach(() => { - cy.visit('http://localhost:3000') + cy.visit('http://127.0.0.1:3000') cy.fixture('statuses.json').then((data) => { statusesFixture = data mockModerationReportsResponse(statusesFixture.multiCidLabeledProfile) @@ -68,8 +68,10 @@ describe('Mod Action -> Label', () => { ) // Validate that events were emitted for 2 different cids matching the exact number of labels added to different cids cy.wait(300).then(() => { - expect(requestedCids).to.have.members(labeledCids) - expect(requestedCids).to.have.lengthOf(labeledCids.length) + const uniqueRequestedCids = [...new Set(requestedCids)] + const uniqueLabeledCids = [...new Set(labeledCids)] + expect(uniqueRequestedCids).to.have.members(uniqueLabeledCids) + expect(uniqueRequestedCids).to.have.lengthOf(uniqueLabeledCids.length) }) }) }) diff --git a/cypress/fixtures/auth.json b/cypress/fixtures/auth.json index 82fd043c..75d3c067 100644 --- a/cypress/fixtures/auth.json +++ b/cypress/fixtures/auth.json @@ -36,22 +36,22 @@ } }, "ozoneMetaResponse": { - "did": "did:plc:ozone", - "url": "https://bsky.social", + "did": "did:plc:ar7c4by46qjdydhdevvrndac", + "url": "http://localhost:2583", "publicKey": "did:key:atprotolabelkey" }, "ozoneDidDataResponse": { - "did": "did:plc:ozone", + "did": "did:plc:ar7c4by46qjdydhdevvrndac", "rotationKeys": [], "alsoKnownAs": ["at://ozone.invalid"], "services": { "atproto_pds": { "type": "AtprotoPersonalDataServer", - "endpoint": "https://bsky.social" + "endpoint": "http://localhost:2583" }, "atproto_labeler": { "type": "AtprotoLabeler", - "endpoint": "https://bsky.social" + "endpoint": "http://localhost:2583" } }, "verificationMethods": { @@ -78,5 +78,143 @@ "viewer": { "role": "tools.ozone.team.defs#roleAdmin" } + }, + "getLabelerRecordResponse": { + "uri": "at://did:plc:ar7c4by46qjdydhdevvrndac/app.bsky.labeler.service/self", + "cid": "bafyreigklczuekgj7q2pj3ys5ck3ah7v2ps5qbly6viwcj26vrmigbfyce", + "value": { + "$type": "app.bsky.labeler.service", + "policies": { + "labelValues": [ + "!hide", + "!warn", + "porn", + "sexual", + "nudity", + "sexual-figurative", + "graphic-media", + "self-harm", + "sensitive", + "extremist", + "intolerant", + "threat" + ], + "labelValueDefinitions": [ + { + "blurs": "content", + "locales": [ + { + "lang": "en", + "name": "Spam", + "description": "Unwanted, repeated, or unrelated actions that bother users." + } + ], + "severity": "inform", + "adultOnly": false, + "identifier": "spam", + "defaultSetting": "hide" + }, + { + "blurs": "none", + "locales": [ + { + "lang": "en", + "name": "Impersonation", + "description": "Pretending to be someone else without permission." + } + ], + "severity": "inform", + "adultOnly": false, + "identifier": "impersonation", + "defaultSetting": "hide" + }, + { + "blurs": "content", + "locales": [ + { + "lang": "en", + "name": "Scam", + "description": "Scams, phishing & fraud." + } + ], + "severity": "alert", + "adultOnly": false, + "identifier": "scam", + "defaultSetting": "hide" + }, + { + "blurs": "content", + "locales": [ + { + "lang": "en", + "name": "Intolerance", + "description": "Discrimination against protected groups." + } + ], + "severity": "alert", + "adultOnly": false, + "identifier": "intolerant", + "defaultSetting": "warn" + }, + { + "blurs": "content", + "locales": [ + { + "lang": "en", + "name": "Self-Harm", + "description": "Promotes self-harm, including graphic images, glorifying discussions, or triggering stories." + } + ], + "severity": "alert", + "adultOnly": false, + "identifier": "self-harm", + "defaultSetting": "warn" + }, + { + "blurs": "content", + "locales": [ + { + "lang": "en", + "name": "Security Concerns", + "description": "May be unsafe and could harm your device, steal your info, or get your account hacked." + } + ], + "severity": "alert", + "adultOnly": false, + "identifier": "security", + "defaultSetting": "hide" + }, + { + "blurs": "content", + "locales": [ + { + "lang": "en", + "name": "Misleading", + "description": "Altered images/videos, deceptive links, or false statements." + } + ], + "severity": "alert", + "adultOnly": false, + "identifier": "misleading", + "defaultSetting": "warn" + }, + { + "blurs": "content", + "locales": [ + { + "lang": "en", + "name": "Threats", + "description": "Promotes violence or harm towards others, including threats, incitement, or advocacy of harm." + } + ], + "severity": "inform", + "adultOnly": false, + "identifier": "threat", + "defaultSetting": "hide" + } + ] + }, + "createdAt": "2024-03-13T15:52:53.522Z" + } } } diff --git a/cypress/fixtures/seed.json b/cypress/fixtures/seed.json index c441ae0d..b0dd266e 100644 --- a/cypress/fixtures/seed.json +++ b/cypress/fixtures/seed.json @@ -148,7 +148,7 @@ "labels": [ { "ver": 1, - "src": "did:plc:ozone", + "src": "did:plc:ar7c4by46qjdydhdevvrndac", "uri": "at://did:plc:jttgywq7eusytkmurmjbum6h/app.bsky.actor.profile/self", "cid": "bafyreifar3ib273ksyfwp4faluaj32aahuc6u2pdnxaygzbkqdhrjejhhi", "val": "porn", @@ -160,7 +160,7 @@ { "ver": 1, - "src": "did:plc:ozone", + "src": "did:plc:ar7c4by46qjdydhdevvrndac", "uri": "at://did:plc:jttgywq7eusytkmurmjbum6h/app.bsky.actor.profile/self", "cid": "bafyreifar3ib273xczfwp4faluaj32aahuc6u2pdnxaygzbkqdhrjejhhi", "val": "porn", diff --git a/cypress/support/api.ts b/cypress/support/api.ts index cc454f48..402ba1d9 100644 --- a/cypress/support/api.ts +++ b/cypress/support/api.ts @@ -1,6 +1,7 @@ -export const API_URL = 'https://bsky.social' +export const API_URL = 'http://localhost:2583' export const SERVER_URL = `${API_URL}/xrpc` export const PLC_URL = 'https://plc.directory' +export const HANDLE_RESOLVER_URL = 'https://api.bsky.app/xrpc' export const mockAuthResponse = (response: Record) => cy.intercept( @@ -35,6 +36,26 @@ export const mockRecordResponse = (response: { ) } +export const mockLabelerServiceRecordResponse = (response: { + statusCode: number + body: Record +}) => { + const [did, collection, rkey] = response.body.uri + .replace('at://', '') + .split('/') + const queryParams = `repo=${encodeURIComponent( + did, + )}&collection=${encodeURIComponent(collection)}&rkey=${encodeURIComponent( + rkey, + )}` + + cy.intercept( + 'GET', + `${SERVER_URL}/com.atproto.repo.getRecord?${queryParams}`, + response, + ) +} + export const mockProfileResponse = (response: { statusCode: number body: Record diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 7d16d844..d4a7d7e2 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -6,6 +6,8 @@ import { mockOzoneDidDataResponse, mockServerConfigResponse, API_URL, + mockRecordResponse, + mockLabelerServiceRecordResponse, } from './api' Cypress.Commands.add( @@ -13,6 +15,7 @@ Cypress.Commands.add( (authFixture: { createSessionResponse: any getRepoResponse: any + getLabelerRecordResponse: any getProfileResponse: any ozoneMetaResponse: any ozoneDidDataResponse: any @@ -43,6 +46,10 @@ Cypress.Commands.add( statusCode: 200, body: authFixture.ozoneDidDataResponse, }) + mockLabelerServiceRecordResponse({ + statusCode: 200, + body: authFixture.getLabelerRecordResponse, + }) cy.get('#service-url').should('have.value', API_URL) cy.get('#account-handle').type('alice.test') diff --git a/environment.d.ts b/environment.d.ts index a2234164..7a47cc24 100644 --- a/environment.d.ts +++ b/environment.d.ts @@ -3,9 +3,12 @@ import Next from 'next' declare global { namespace NodeJS { interface ProcessEnv { + NEXT_PUBLIC_HANDLE_RESOLVER_URL?: string // e.g. https://resolver.example.com NEXT_PUBLIC_PLC_DIRECTORY_URL?: string // e.g. https://plc.directory NEXT_PUBLIC_QUEUE_CONFIG?: string NEXT_PUBLIC_OZONE_SERVICE_DID?: string // e.g. did:plc:xxx#atproto_labeler + NEXT_PUBLIC_OZONE_PUBLIC_URL?: string // e.g. https://ozone.example.com (falls back to window.location.origin) + NEXT_PUBLIC_SOCIAL_APP_URL?: string // e.g. https://bsky.app } } } diff --git a/lib/client-config.ts b/lib/client-config.ts index e562cc22..15b5d01d 100644 --- a/lib/client-config.ts +++ b/lib/client-config.ts @@ -1,37 +1,49 @@ import { AppBskyLabelerService } from '@atproto/api' +import { OZONE_PUBLIC_URL, OZONE_SERVICE_DID } from './constants' import { DidDocData, resolveDidDocData } from './identity' -export async function getConfig(labelerDid?: string): Promise { +export async function getConfig(): Promise { let doc: DidDocData | null = null let meta: OzoneMeta | null = null - labelerDid = labelerDid?.split('#')[0] // ensure no service id + const labelerDid = OZONE_SERVICE_DID?.split('#')[0] // ensure no service id if (labelerDid) { doc = await resolveDidDocData(labelerDid) const labelerUrl = doc && getServiceUrlFromDoc(doc, 'atproto_labeler') - if (labelerUrl) { - meta = await getOzoneMeta(labelerUrl) + if (process.env.NODE_ENV === 'development' && doc && labelerUrl) { + meta = { + did: labelerDid, + url: labelerUrl, + publicKey: getDidKeyFromDoc(doc, 'atproto_label')!, + } } else { - meta = await getOzoneMeta() + meta = await getOzoneMeta( + labelerUrl || OZONE_PUBLIC_URL || window.location.origin, + ) } } else { - meta = await getOzoneMeta() + meta = await getOzoneMeta(OZONE_PUBLIC_URL || window.location.origin) if (meta) { doc = await resolveDidDocData(meta.did) } } - labelerDid ??= meta?.did - if (!labelerDid) { + + const did = meta?.did ?? labelerDid + + if (!did) { throw new Error('Could not determine an Ozone service DID') + } else if (labelerDid && did !== labelerDid) { + throw new Error( + 'Mismatch between Ozone service DID and Ozone service metadata', + ) } + const labelerUrl = doc && getServiceUrlFromDoc(doc, 'atproto_labeler') const labelerKey = doc && getDidKeyFromDoc(doc, 'atproto_label') const handle = doc && getHandleFromDoc(doc) const pdsUrl = doc && getServiceUrlFromDoc(doc, 'atproto_pds') - const record = pdsUrl - ? await getLabelerServiceRecord(pdsUrl, labelerDid) - : null + const record = pdsUrl ? await getLabelerServiceRecord(pdsUrl, did) : null return { - did: labelerDid, + did, doc, meta, handle, @@ -54,7 +66,7 @@ export async function getConfig(labelerDid?: string): Promise { } } -async function getOzoneMeta(serviceUrl = window.location.origin) { +async function getOzoneMeta(serviceUrl: string) { try { const url = new URL('/.well-known/ozone-metadata.json', serviceUrl) const res = await fetch(url) @@ -75,10 +87,7 @@ function getHandleFromDoc(doc: DidDocData) { return handleAka.replace('at://', '') } -export function getDidKeyFromDoc( - doc: DidDocData, - keyId: string, -): string | null { +function getDidKeyFromDoc(doc: DidDocData, keyId: string): string | null { return doc.verificationMethods[keyId] ?? null } @@ -113,7 +122,7 @@ export function withDocAndMeta(config: OzoneConfig) { return config as OzoneConfigFull } -export type OzoneMeta = { did: string; url: string; publicKey: string } +type OzoneMeta = { did: string; url: string; publicKey: string } export type OzoneConfig = { did: string diff --git a/lib/client.ts b/lib/client.ts index 3edcd411..b6038848 100644 --- a/lib/client.ts +++ b/lib/client.ts @@ -1,268 +1,9 @@ -import { AtpAgent, Agent, AtpSessionData } from '@atproto/api' -import { AuthState } from './types' -import { OzoneConfig, getConfig } from './client-config' -import { OZONE_SERVICE_DID } from './constants' -import { getExternalLabelers } from '@/config/data' -import { parseServerConfig, ServerConfig } from './server-config' -import { toast } from 'react-toastify' - -export interface ClientSession extends AtpSessionData { - service: string - config: OzoneConfig - serverConfig: ServerConfig - skipRecord: boolean -} +import { AtpAgent } from '@atproto/api' +import { HANDLE_RESOLVER_URL } from './constants' // exported api // = -class ClientManager extends EventTarget { - hasSetup = false - private _agent: AtpAgent | undefined - private _session: ClientSession | undefined - - get authState() { - if (!this._agent || !this._session) { - return AuthState.LoggedOut - } - const { config, skipRecord } = this._session - if ( - config.needs.key || - config.needs.service || - (!skipRecord && config.needs.record) - ) { - return AuthState.LoggedInUnconfigured - } - return AuthState.LoggedIn - } - - get api(): Agent { - if (this._agent) { - return this._agent.api - } - throw new Error('Not authed') - } - - get session(): ClientSession { - if (this._session) { - return this._session - } - throw new Error('Not authed') - } - - // this gets called by the login modal during initial render - async setup() { - if (this.hasSetup) return this.authState - this._session = _loadSession() - await this._setup() - this.hasSetup = true - this._emit('change') - return this.authState - } - - async signin( - service: string, - handle: string, - password: string, - authFactorToken: string = '', - ) { - const agent = new AtpAgent({ - service, - persistSession: (_type, session) => { - this._onSessionChange(session) - }, - }) - - const { data: login } = await agent.login({ - identifier: handle, - authFactorToken, - password, - }) - const config = await this._getConfig() - // Cannot make authed requests until the labeler's DID document reflects a - // service URL. We will use an empty server config until the user goes through - // the configuration flow to setup their DID document with a service URL. - const serverConfig = config.needs.service - ? parseServerConfig({}) - : await this._getServerConfig(agent, config.did) - this._session = { - service, - config, - serverConfig, - skipRecord: config.did !== login.did, // skip if not logged-in as service account - accessJwt: login.accessJwt, - refreshJwt: login.refreshJwt, - handle: login.handle, - did: login.did, - active: login.active !== false, - } - this._agent = agent - _saveSession(this._session) - this._emit('change') - return this.authState - } - - async signout() { - try { - this._agent?.api.com.atproto.server.deleteSession(undefined, { - headers: { - authorization: `Bearer ${this.session.refreshJwt}`, - }, - }) - } catch (err) { - console.error('(Minor issue) Failed to delete session on the server', err) - } - this._clear() - this._emit('change') - return this.authState - } - - async reconfigure(opts?: { skipRecord?: boolean }) { - if (!this._session) return - const config = await this._getConfig(this._session.config.did) - this._session = { - ...this._session, - config, - skipRecord: opts?.skipRecord ?? this._session.skipRecord, - } - _saveSession(this._session) - this._emit('change') - return this.authState - } - - getServiceDid(override?: string) { - return override ?? (this._session?.config.did || OZONE_SERVICE_DID) - } - - proxyHeaders(override?: string): Record { - const proxy = this.getServiceDid(override) - const externalLabelers = getExternalLabelers() - const labelerDids = Object.keys(externalLabelers).join(',') - - return proxy - ? { - 'atproto-proxy': _ensureServiceId(proxy), - 'atproto-accept-labelers': labelerDids, - } - : {} - } - - private async _setup() { - if (this._session) { - const agent = new AtpAgent({ - service: this._session.service, - persistSession: (_type, session) => { - this._onSessionChange(session) - }, - }) - await agent.resumeSession(this._session) - this._agent = agent - } else { - this._agent = undefined - } - } - - private async _getConfig(ozoneDid?: string) { - const builtIn = ozoneDid || OZONE_SERVICE_DID - return await getConfig(builtIn) - } - - private async _getServerConfig(agent: AtpAgent, ozoneDid: string) { - const createUnAuthorizedError = () => { - throw new Error( - "Account does not have access to this Ozone service. If this seems in error, check Ozone's access configuration.", - ) - } - try { - const { data } = await agent.api.tools.ozone.server.getConfig( - {}, - { headers: this.proxyHeaders(ozoneDid) }, - ) - if (!data.viewer?.role) throw createUnAuthorizedError() - return parseServerConfig(data) - } catch (err) { - if (err?.['status'] === 401) { - throw createUnAuthorizedError() - } - throw err - } - } - - async refetchServerConfig() { - if (!this._session || !this._agent) { - toast.error(`Must be logged in to fetch server config`) - return - } - const serverConfig = await this._getServerConfig( - this._agent, - this._session?.config.did, - ) - this._onSessionChange({ ...this._session, serverConfig }) - } - - private _onSessionChange(newSession?: AtpSessionData | ClientSession) { - if (newSession && this._session) { - Object.assign(this._session, newSession) - _saveSession(this._session) - } else { - this._clear() - this._emit('change') - } - } - - private _clear() { - _deleteSession() - this._agent = undefined - this._session = undefined - } - - private _emit(type: string) { - this.dispatchEvent(new Event(type)) - } -} -const clientManager = new ClientManager() -export default clientManager - -// For debugging and low-level access -;(globalThis as any).client = clientManager - -// helpers -// = - -const SESSION_KEY = 'ozone_session' - -function _loadSession(): ClientSession | undefined { - try { - const str = localStorage.getItem(SESSION_KEY) - const obj = str ? JSON.parse(str) : undefined - if (!obj || typeof obj === 'undefined') { - return undefined - } - if ( - !obj.service || - !obj.refreshJwt || - !obj.accessJwt || - !obj.handle || - !obj.did || - !obj.config - ) { - return undefined - } - return obj as ClientSession - } catch (e) { - return undefined - } -} - -function _saveSession(session: ClientSession) { - localStorage.setItem(SESSION_KEY, JSON.stringify(session)) -} - -function _deleteSession() { - localStorage.removeItem(SESSION_KEY) -} - -function _ensureServiceId(did: string) { - if (did.includes('#')) return did - return `${did}#atproto_labeler` -} +export const globalAgent = new AtpAgent({ + service: HANDLE_RESOLVER_URL, +}) diff --git a/lib/constants.ts b/lib/constants.ts index a74bdc78..6c8a2c3a 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -1,13 +1,30 @@ +export const OAUTH_SCOPE = 'atproto transition:generic' + export const OZONE_SERVICE_DID = process.env.NEXT_PUBLIC_OZONE_SERVICE_DID || undefined +export const OZONE_PUBLIC_URL = + process.env.NEXT_PUBLIC_OZONE_PUBLIC_URL || undefined + export const PLC_DIRECTORY_URL = - process.env.NEXT_PUBLIC_PLC_DIRECTORY_URL || `https://plc.directory` + process.env.NEXT_PUBLIC_PLC_DIRECTORY_URL || + (process.env.NODE_ENV === 'development' + ? 'http://localhost:2582' + : 'https://plc.directory') export const QUEUE_CONFIG = process.env.NEXT_PUBLIC_QUEUE_CONFIG || '{}' -export const SOCIAL_APP_DOMAIN = 'bsky.app' -export const SOCIAL_APP_URL = `https://${SOCIAL_APP_DOMAIN}` +export const SOCIAL_APP_URL = + process.env.NEXT_PUBLIC_SOCIAL_APP_URL || + (process.env.NODE_ENV === 'development' + ? 'http://localhost:2584' + : 'https://bsky.app') + +export const HANDLE_RESOLVER_URL = + process.env.NEXT_PUBLIC_HANDLE_RESOLVER_URL || + (process.env.NODE_ENV === 'development' + ? 'http://localhost:2584' + : 'https://api.bsky.app') export const DM_DISABLE_TAG = 'chat-disabled' export const VIDEO_UPLOAD_DISABLE_TAG = 'video-upload-disabled' diff --git a/lib/identity.ts b/lib/identity.ts index 3fa4f5fb..5704b4e6 100644 --- a/lib/identity.ts +++ b/lib/identity.ts @@ -1,15 +1,11 @@ -import clientManager from './client' +import { globalAgent } from './client' import { PLC_DIRECTORY_URL } from './constants' export const getDidFromHandle = async ( handle: string, ): Promise => { try { - const { data } = await clientManager.api.com.atproto.identity.resolveHandle( - { - handle, - }, - ) + const { data } = await globalAgent.resolveHandle({ handle }) return data.did } catch (err) { return null @@ -18,10 +14,11 @@ export const getDidFromHandle = async ( export const resolveDidDocData = async function ( did: string, + signal?: AbortSignal, ): Promise { if (did.startsWith('did:plc:')) { const url = new URL(`/${did}/data`, PLC_DIRECTORY_URL) - const res = await fetch(url) + const res = await fetch(url, { signal }) if (res.status !== 200) return null const doc = await res.json() return doc @@ -29,7 +26,7 @@ export const resolveDidDocData = async function ( if (did.startsWith('did:web:')) { const hostname = did.slice('did:web:'.length) const url = new URL(`/.well-known/did.json`, hostname) - const res = await fetch(url) + const res = await fetch(url, { signal }) if (res.status !== 200) return null const doc = await res.json().catch(() => null) if (!doc || typeof doc !== 'object' || doc['id'] !== did) return null @@ -38,7 +35,10 @@ export const resolveDidDocData = async function ( return null } -function didDocToData(doc: { id: string; [key: string]: unknown }): DidDocData { +export function didDocToData(doc: { + id: string + [key: string]: unknown +}): DidDocData { return { did: doc.id, alsoKnownAs: Array.isArray(doc['alsoKnownAs']) ? doc['alsoKnownAs'] : [], diff --git a/lib/local-storage.ts b/lib/local-storage.ts index d7b8e8b5..eaa53587 100644 --- a/lib/local-storage.ts +++ b/lib/local-storage.ts @@ -1,10 +1,11 @@ +// TODO : replace this with useLocalStorage from 'react-use' + const getItem = (key: string) => localStorage.getItem(key) -const setItem = (key: string, value: string) => - localStorage.setItem(key, value) +const setItem = (key: string, value: string) => localStorage.setItem(key, value) -export const getLocalStorageData = (key: string): T | null => { +export const getLocalStorageData = (key: string): T | undefined => { const data = getItem(key) - if (!data) return null + if (!data) return undefined try { return JSON.parse(data) } catch (err) { diff --git a/lib/server-config.ts b/lib/server-config.ts index 2ee48e98..174269aa 100644 --- a/lib/server-config.ts +++ b/lib/server-config.ts @@ -1,11 +1,11 @@ import { ToolsOzoneServerGetConfig, ToolsOzoneTeamDefs } from '@atproto/api' -import client from './client' export type ServerConfig = { pds?: string appview?: string blobDivert?: string chat?: string + role?: ToolsOzoneServerGetConfig.ViewerConfig['role'] permissions: { canManageTemplates: boolean canTakedown: boolean @@ -18,8 +18,10 @@ export type ServerConfig = { } } +export type PermissionName = keyof ServerConfig['permissions'] + export const parseServerConfig = ( - config: ToolsOzoneServerGetConfig.Response['data'], + config: ToolsOzoneServerGetConfig.OutputSchema, ): ServerConfig => { const isAdmin = config.viewer?.role === ToolsOzoneTeamDefs.ROLEADMIN const isModerator = @@ -30,6 +32,7 @@ export const parseServerConfig = ( blobDivert: config.blobDivert?.url, appview: config.appview?.url, chat: config.chat?.url, + role: config.viewer?.role, permissions: { canManageTemplates: isModerator, canTakedown: !!config.pds?.url && isModerator, @@ -42,15 +45,3 @@ export const parseServerConfig = ( }, } } - -export const checkPermission = ( - permission: keyof ServerConfig['permissions'], -) => { - try { - return client.session.serverConfig?.permissions[permission] - } catch (e) { - // Trying to access client.session while unauthenticated throws an error in which case, - // we can safely assume the user does not have the permission - return false - } -} diff --git a/lib/types.ts b/lib/types.ts index e501eeb3..355e7fde 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -33,10 +33,3 @@ export type PropsOf = F extends ( ) => ReactNode ? P : never - -export enum AuthState { - Validating, - LoggedIn, - LoggedInUnconfigured, - LoggedOut, -} diff --git a/lib/useCallbackRef.ts b/lib/useCallbackRef.ts new file mode 100644 index 00000000..4c72e950 --- /dev/null +++ b/lib/useCallbackRef.ts @@ -0,0 +1,27 @@ +import { useCallback } from 'react' + +import { useValueRef } from './useValueRef' + +export function useCallbackRef any>( + fn: T, +): (this: ThisParameterType, ...args: Parameters) => ReturnType + +export function useCallbackRef any>( + fn?: T, +): (this: ThisParameterType, ...args: Parameters) => void | ReturnType + +export function useCallbackRef any>( + fn?: T, +) { + const fnRef = useValueRef(fn) + return useCallback( + function ( + this: ThisParameterType, + ...args: Parameters + ): void | ReturnType { + const { current } = fnRef + if (current) return current.call(this, ...args) + }, + [fnRef], + ) +} diff --git a/lib/useSession.ts b/lib/useSession.ts deleted file mode 100644 index de0f7d30..00000000 --- a/lib/useSession.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useEffect, useState } from 'react' -import client from './client' -import { AuthState } from './types' - -export function useSession() { - const [session, setSession] = useState( - client.authState === AuthState.LoggedOut ? null : client.session, - ) - useEffect(() => { - const updateSession = () => - setSession( - client.authState === AuthState.LoggedOut ? null : client.session, - ) - client.addEventListener('change', updateSession) - return () => client.removeEventListener('change', updateSession) - }, []) - return session -} diff --git a/lib/useSignaledEffect.ts b/lib/useSignaledEffect.ts new file mode 100644 index 00000000..e6665d37 --- /dev/null +++ b/lib/useSignaledEffect.ts @@ -0,0 +1,38 @@ +import { DependencyList, useEffect } from 'react' + +/** + * @example + * + * ```ts + * const url = 'https://api.example.com/data'; + * + * useSignaledEffect((signal) => { + * fetch(url, { signal }) + * .then(() => { + * // handle response + * }) + * .catch(reason => { + * if (!signal.aborted) { + * // handle failure + * } else { + * // handle abort (optional) + * } + * }); + * }, [url]); + * ``` + */ +export function useSignaledEffect( + fn: (signal: AbortSignal) => void | (() => void), + deps?: DependencyList, +) { + /* eslint-disable react-hooks/exhaustive-deps */ + useEffect(() => { + const controller = new AbortController() + const cleanup = fn(controller.signal) + return () => { + controller.abort() + cleanup?.() + } + }, deps) + /* eslint-enable react-hooks/exhaustive-deps */ +} diff --git a/lib/useValueRef.ts b/lib/useValueRef.ts new file mode 100644 index 00000000..c0e29cb5 --- /dev/null +++ b/lib/useValueRef.ts @@ -0,0 +1,9 @@ +import { useEffect, useRef } from 'react' + +export function useValueRef(value: T) { + const valueRef = useRef(value) + useEffect(() => { + valueRef.current = value + }, [value]) + return valueRef +} diff --git a/lib/util.ts b/lib/util.ts index 0ca79698..46dbe4b0 100644 --- a/lib/util.ts +++ b/lib/util.ts @@ -133,9 +133,9 @@ export const getFragmentsFromBlueSkyAppUrl = (url: string) => { export const buildAtUriFromFragments = ( fragments: BlueSkyAppUrlFragments | null, ) => { - if (fragments?.did) { + if (fragments?.did || fragments?.handle) { const uri = AtUri.make( - `${fragments?.did}`, + `${fragments?.did || fragments?.handle}`, fragments.collection, fragments.rkey, ) diff --git a/middleware.ts b/middleware.ts deleted file mode 100644 index c0bd3cbc..00000000 --- a/middleware.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NextResponse } from 'next/server' -import type { NextRequest } from 'next/server' - -export const config = { - // by only matching on the route where it's needed, - // does not interfere with /xrpc (in particular websockets) - matcher: '/', -} - -export function middleware(request: NextRequest) { - const url = request.nextUrl.clone() - if (url.pathname === '/') { - url.pathname = '/reports' - url.searchParams.set('resolved', 'false') - return NextResponse.redirect(url) - } -} diff --git a/package.json b/package.json index f2696fe2..2f68f4fa 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,10 @@ "e2e:run": "$(yarn bin)/cypress run --browser chrome" }, "dependencies": { - "@atproto/api": "0.13.5", + "@atproto/api": "^0.13.5", + "@atproto/oauth-client-browser": "^0.2.0", + "@atproto/oauth-types": "^0.1.4", + "@atproto/xrpc": "^0.6.1", "@headlessui/react": "^1.7.7", "@heroicons/react": "^2.0.13", "@tanstack/react-query": "^4.22.0", @@ -27,8 +30,8 @@ "@uiw/react-md-editor": "^3.23.5", "bcp-47-match": "^2.0.3", "date-fns": "^2.29.3", - "eslint": "8.30.0", - "eslint-config-next": "13.4.8", + "eslint": "^8.57.0", + "eslint-config-next": "^14.2.4", "hls.js": "^1.5.13", "kbar": "^0.1.0-beta.45", "lande": "^1.0.10", @@ -41,7 +44,7 @@ "react-use": "^17.4.0", "remark": "^14.0.3", "remark-html": "^15.0.2", - "typescript": "4.9.4", + "typescript": "^5.4.5", "yet-another-react-lightbox": "^3.20.0" }, "devDependencies": { @@ -52,6 +55,9 @@ "postcss": "^8.4.20", "tailwindcss": "^3.2.4" }, + "resolutions": { + "jackspeak": "2.1.1" + }, "volta": { "node": "20.9.0" } diff --git a/service/index.js b/service/index.js index 1b7a2219..943c2811 100644 --- a/service/index.js +++ b/service/index.js @@ -38,8 +38,9 @@ async function main() { publicKey: ozone.ctx.signingKey.did(), }) }) - ozone.app.get('*', (req, res) => { - return frontendHandler(req, res) + // Note: We must use `use()` here. This should be the last middleware. + ozone.app.use((req, res) => { + void frontendHandler(req, res, undefined) }) // run const httpServer = await ozone.start() diff --git a/service/package.json b/service/package.json index 5fa85852..1ab0b574 100644 --- a/service/package.json +++ b/service/package.json @@ -3,7 +3,7 @@ "description": "Ozone service entrypoint", "main": "index.js", "dependencies": { - "@atproto/ozone": "0.1.38", + "@atproto/ozone": "0.1.42", "next": "14.2.5" } } diff --git a/service/yarn.lock b/service/yarn.lock index 2e8c1779..807ed07d 100644 --- a/service/yarn.lock +++ b/service/yarn.lock @@ -2,15 +2,15 @@ # yarn lockfile v1 -"@atproto/api@^0.13.1": - version "0.13.1" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.1.tgz#fbf4306e4465d5467aaf031308c1b47dcc8039d0" - integrity sha512-DL3iBfavn8Nnl48FmnAreQB0k0cIkW531DJ5JAHUCQZo10Nq0ZLk2/WFxcs0KuBG5wuLnGUdo+Y6/GQPVq8dYw== +"@atproto/api@^0.13.5": + version "0.13.5" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.5.tgz#04305cdb0a467ba366305c5e95cebb7ce0d39735" + integrity sha512-yT/YimcKYkrI0d282Zxo7O30OSYR+KDW89f81C6oYZfDRBcShC1aniVV8kluP5LrEAg8O27yrOSnBgx2v7XPew== dependencies: "@atproto/common-web" "^0.3.0" "@atproto/lexicon" "^0.4.1" "@atproto/syntax" "^0.3.0" - "@atproto/xrpc" "^0.6.0" + "@atproto/xrpc" "^0.6.1" await-lock "^2.2.2" multiformats "^9.9.0" tlds "^1.234.0" @@ -58,22 +58,22 @@ one-webcrypto "^1.0.3" uint8arrays "3.0.0" -"@atproto/crypto@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@atproto/crypto/-/crypto-0.4.0.tgz#dcdd6bf5ba98261ae0ff3b96d7b8695c1ef788e6" - integrity sha512-Kj/4VgJ7hzzXvE42L0rjzP6lM0tai+OfPnP1rxJ+UZg/YUDtuewL4uapnVoWXvlNceKgaLZH98g5n9gXBVTe5Q== +"@atproto/crypto@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@atproto/crypto/-/crypto-0.4.1.tgz#c9d3095258d198918cd25ba8b8bb27417f12e1bb" + integrity sha512-7pQNHWYyx8jGhYdPbmcuPD9W73nd/5v3mfBlncO0sBzxnPbmA6aXAWOz+fNVZwHwBJPeb/Gzf/FT/uDx7/eYFg== dependencies: "@noble/curves" "^1.1.0" "@noble/hashes" "^1.3.1" uint8arrays "3.0.0" -"@atproto/identity@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@atproto/identity/-/identity-0.4.0.tgz#f8a4d450a20606d221c4ec05b856c0ce55f0a3a7" - integrity sha512-KKdVlqBgkFuTUx3KFiiQe0LuK9kopej1bhKm6SHRPEYbSEPFmRZQMY9TAjWJQrvQt8DpQzz6kVGjASFEjd3teQ== +"@atproto/identity@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@atproto/identity/-/identity-0.4.1.tgz#749e2776bda71b938b7aa4f1966471ad3fd5039c" + integrity sha512-5AoPJDSD0rAay/6Sib+n/FjfwGulM/+xCNxwwDLR9QI4EoeUlvIH8g5BNdix812v312/Qd42kJrLpCNTZ5rvew== dependencies: "@atproto/common-web" "^0.3.0" - "@atproto/crypto" "^0.4.0" + "@atproto/crypto" "^0.4.1" axios "^0.27.2" "@atproto/lexicon@^0.4.1": @@ -87,19 +87,19 @@ multiformats "^9.9.0" zod "^3.23.8" -"@atproto/ozone@0.1.38": - version "0.1.38" - resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.38.tgz#489452b2eaaaadcace78a73da9f8ad98d04ccd9b" - integrity sha512-SEGyds7Uc04d1f5qdLFvc0CyxYFyN+fHEmBsSqEombJec5aVaEel7+EXoOtgEHCRD4vUPi8MH3kqHIKp+YYtAA== +"@atproto/ozone@0.1.42": + version "0.1.42" + resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.1.42.tgz#d2630e990895f2c132ece759d530f1b0ba3685dc" + integrity sha512-Fi0pGmiGZrXrd2NQJp9zFEu1XoDiFTqUu96KZUC647teBXvHCEurK3OfrjsbcQYpxm2gcDVDyrVTs7PoKpkQ1Q== dependencies: - "@atproto/api" "^0.13.1" + "@atproto/api" "^0.13.5" "@atproto/common" "^0.4.1" - "@atproto/crypto" "^0.4.0" - "@atproto/identity" "^0.4.0" + "@atproto/crypto" "^0.4.1" + "@atproto/identity" "^0.4.1" "@atproto/lexicon" "^0.4.1" "@atproto/syntax" "^0.3.0" - "@atproto/xrpc" "^0.6.0" - "@atproto/xrpc-server" "^0.6.2" + "@atproto/xrpc" "^0.6.1" + "@atproto/xrpc-server" "^0.6.3" "@did-plc/lib" "^0.0.1" axios "^1.6.7" compression "^1.7.4" @@ -121,15 +121,15 @@ resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.3.0.tgz#fafa2dbea9add37253005cb663e7373e05e618b3" integrity sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA== -"@atproto/xrpc-server@^0.6.2": - version "0.6.2" - resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.6.2.tgz#6d16dc1f0da5b81ef4b0b3e633741235c7ef5617" - integrity sha512-WuY0fCU/GHp1Obeikh+G4a39HnvhoxFndhQgA4Nb2hh1YOnPN48RHsGOctwA63N11mqk0pGX24vP56ozz0DbFw== +"@atproto/xrpc-server@^0.6.3": + version "0.6.3" + resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.6.3.tgz#779f5eb53745a94fe816a919febf52fc2638ddfc" + integrity sha512-0YXeBM9NjiIlR5eXWo8qzArRcBOKhwVimpH+ajKgZzlncPO53brVZ9+3BUnD5J1PG8mEQFRERi+Jt77QyF89qA== dependencies: "@atproto/common" "^0.4.1" - "@atproto/crypto" "^0.4.0" + "@atproto/crypto" "^0.4.1" "@atproto/lexicon" "^0.4.1" - "@atproto/xrpc" "^0.6.0" + "@atproto/xrpc" "^0.6.1" cbor-x "^1.5.1" express "^4.17.2" http-errors "^2.0.0" @@ -139,10 +139,10 @@ ws "^8.12.0" zod "^3.23.8" -"@atproto/xrpc@^0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.0.tgz#668c3262e67e2afa65951ea79a03bfe3720ddf5c" - integrity sha512-5BbhBTv5j6MC3iIQ4+vYxQE7nLy2dDGQ+LYJrH8PptOCUdq0Pwg6aRccQ3y52kUZlhE/mzOTZ8Ngiy9pSAyfVQ== +"@atproto/xrpc@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.1.tgz#dcd1315c8c60eef5af2db7fa4e35a38ebc6d79d5" + integrity sha512-Zy5ydXEdk6sY7FDUZcEVfCL1jvbL4tXu5CcdPqbEaW6LQtk9GLds/DK1bCX9kswTGaBC88EMuqQMfkxOhp2t4A== dependencies: "@atproto/lexicon" "^0.4.1" zod "^3.23.8" diff --git a/tsconfig.json b/tsconfig.json index fb4b7dae..ccca6d5f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -36,6 +36,13 @@ "@/workspace/*": ["components/workspace/*"], } }, - "include": ["next-env.d.ts", "**/**/**/*.tsx", "**/**/**/*.ts", ".next/types/**/*.ts"], + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + // Folder with a dot in the name is not included by default (?) + "**/.*/**/*.ts", + ".next/types/**/*.ts" + ], "exclude": ["node_modules", "cypress/*"] } diff --git a/yarn.lock b/yarn.lock index 2d3d9a2b..b0248dd2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,7 +2,65 @@ # yarn lockfile v1 -"@atproto/api@0.13.5": +"@atproto-labs/did-resolver@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@atproto-labs/did-resolver/-/did-resolver-0.1.2.tgz#0dc5fb9cb1a80061a5513f3741a8fa8f6cc31f57" + integrity sha512-d/nQHoieDo0tf0OX45LJcLQlSuyzVOV5lND7krlSxeAyD3pO5Fx1G8FtmkoPlMt4LT1OCIIQNmjh42pOcGH3WA== + dependencies: + "@atproto-labs/fetch" "0.1.0" + "@atproto-labs/pipe" "0.1.0" + "@atproto-labs/simple-store" "0.1.1" + "@atproto-labs/simple-store-memory" "0.1.1" + "@atproto/did" "0.1.1" + zod "^3.23.8" + +"@atproto-labs/fetch@0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@atproto-labs/fetch/-/fetch-0.1.0.tgz#50a46943fd2f321dd748de28c73ba7cbfa493132" + integrity sha512-uirja+uA/C4HNk7vayM+AJqsccxQn2wVziUHxbsjJGt/K6Q8ZOKDaEX2+GrcXvpUVcqUKh+94JFjuzH+CAEUlg== + dependencies: + "@atproto-labs/pipe" "0.1.0" + optionalDependencies: + zod "^3.23.8" + +"@atproto-labs/handle-resolver@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@atproto-labs/handle-resolver/-/handle-resolver-0.1.2.tgz#045ee5cf881538f28f3b058856237bc524e44f64" + integrity sha512-0D8d1QpGqyp0DLYnKpAFJ5YaIgiRUHMqKnbd1d0reOuJoa7ebwxMolNhP3RnKlOQ/9gaL3Y3ORZFeEjXK+eRqg== + dependencies: + "@atproto-labs/simple-store" "0.1.1" + "@atproto-labs/simple-store-memory" "0.1.1" + "@atproto/did" "0.1.1" + zod "^3.23.8" + +"@atproto-labs/identity-resolver@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@atproto-labs/identity-resolver/-/identity-resolver-0.1.2.tgz#7becc62774d16a037acc3b25e15aa997ec62fc6c" + integrity sha512-166XTfq/gvdzmJT6tMvMvsT4h9yVyse8yJVn534j5GPGTqPtyky57/SNyO+R8QbOr4ffG0NQRO+OAazsVR0mVw== + dependencies: + "@atproto-labs/did-resolver" "0.1.2" + "@atproto-labs/handle-resolver" "0.1.2" + "@atproto/syntax" "0.3.0" + +"@atproto-labs/pipe@0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@atproto-labs/pipe/-/pipe-0.1.0.tgz#c8d86923b6d8e900d39efe6fdcdf0d897c434086" + integrity sha512-ghOqHFyJlQVFPESzlVHjKroP0tPzbmG5Jms0dNI9yLDEfL8xp4OFPWLX4f6T8mRq69wWs4nIDM3sSsFbFqLa1w== + +"@atproto-labs/simple-store-memory@0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@atproto-labs/simple-store-memory/-/simple-store-memory-0.1.1.tgz#54526a1f8ec978822be9fad75106ad8b78500dd3" + integrity sha512-PCRqhnZ8NBNBvLku53O56T0lsVOtclfIrQU/rwLCc4+p45/SBPrRYNBi6YFq5rxZbK6Njos9MCmILV/KLQxrWA== + dependencies: + "@atproto-labs/simple-store" "0.1.1" + lru-cache "^10.2.0" + +"@atproto-labs/simple-store@0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@atproto-labs/simple-store/-/simple-store-0.1.1.tgz#e743a2722b5d8732166f0a72aca8bd10e9bff106" + integrity sha512-WKILW2b3QbAYKh+w5U2x6p5FqqLl0nAeLwGeDY+KjX01K4Dq3vQTR9b/qNp0jZm48CabPQVrqCv0PPU9LgRRRg== + +"@atproto/api@^0.13.5": version "0.13.5" resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.5.tgz#04305cdb0a467ba366305c5e95cebb7ce0d39735" integrity sha512-yT/YimcKYkrI0d282Zxo7O30OSYR+KDW89f81C6oYZfDRBcShC1aniVV8kluP5LrEAg8O27yrOSnBgx2v7XPew== @@ -25,6 +83,37 @@ uint8arrays "3.0.0" zod "^3.21.4" +"@atproto/did@0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@atproto/did/-/did-0.1.1.tgz#377fae1793ea9389da73dd393e257fc766b526c5" + integrity sha512-FA+U8C8ACQLjG/TSgtaQyjvXxzOYzwK0+T6FJ1oj2BtKUixq4t8zpvo4zdIrnVimXeGQWo1/U1ghke58SmRpmQ== + dependencies: + zod "^3.23.8" + +"@atproto/jwk-jose@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@atproto/jwk-jose/-/jwk-jose-0.1.2.tgz#236eadb740b498689d9a912d1254aa9ff58890a1" + integrity sha512-lDwc/6lLn2aZ/JpyyggyjLFsJPMntrVzryyGUx5aNpuTS8SIuc4Ky0REhxqfLopQXJJZCuRRjagHG3uP05/moQ== + dependencies: + "@atproto/jwk" "0.1.1" + jose "^5.2.0" + +"@atproto/jwk-webcrypto@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@atproto/jwk-webcrypto/-/jwk-webcrypto-0.1.2.tgz#d11ebd48575f3d59759ec350102038efd57dbed9" + integrity sha512-vTBUbUZXh0GI+6KJiPGukmI4BQEHFAij8fJJ4WnReF/hefAs3ISZtrWZHGBebz+q2EcExYlnhhlmxvDzV7veGw== + dependencies: + "@atproto/jwk" "0.1.1" + "@atproto/jwk-jose" "0.1.2" + +"@atproto/jwk@0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@atproto/jwk/-/jwk-0.1.1.tgz#15bcad4a1778eeb20c82108e0ec55fef45cd07b6" + integrity sha512-6h/bj1APUk7QcV9t/oA6+9DB5NZx9SZru9x+/pV5oHFI9Xz4ZuM5+dq1PfsJV54pZyqdnZ6W6M717cxoC7q7og== + dependencies: + multiformats "^9.9.0" + zod "^3.23.8" + "@atproto/lexicon@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.1.tgz#19155210570a2fafbcc7d4f655d9b813948e72a0" @@ -36,12 +125,52 @@ multiformats "^9.9.0" zod "^3.23.8" -"@atproto/syntax@^0.3.0": +"@atproto/oauth-client-browser@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@atproto/oauth-client-browser/-/oauth-client-browser-0.2.0.tgz#d93702cd7da3f68a55296e4a1a019f99b07bdd8f" + integrity sha512-xNnk6efAYGydgyweOnnDPccnvpHFyyWf8UfXoSrpdzSgIDZa8Eo35J9cyd2nAC1uP929QL2sKW/OWQZwD+/mcA== + dependencies: + "@atproto-labs/did-resolver" "0.1.2" + "@atproto-labs/handle-resolver" "0.1.2" + "@atproto-labs/simple-store" "0.1.1" + "@atproto/did" "0.1.1" + "@atproto/jwk" "0.1.1" + "@atproto/jwk-webcrypto" "0.1.2" + "@atproto/oauth-client" "0.2.0" + "@atproto/oauth-types" "0.1.4" + +"@atproto/oauth-client@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@atproto/oauth-client/-/oauth-client-0.2.0.tgz#f72a8b76acbc2a1bcb7356b218cbc83d7a80eabf" + integrity sha512-J8NaQ45jIa7e/VXKUwtGnW91p8mM8yWvqhlBxNvVXQ2t5yo6rxYdIVam+2Ffy3SANxqe+sl5QFmCdaJQGX0yNQ== + dependencies: + "@atproto-labs/did-resolver" "0.1.2" + "@atproto-labs/fetch" "0.1.0" + "@atproto-labs/handle-resolver" "0.1.2" + "@atproto-labs/identity-resolver" "0.1.2" + "@atproto-labs/simple-store" "0.1.1" + "@atproto-labs/simple-store-memory" "0.1.1" + "@atproto/did" "0.1.1" + "@atproto/jwk" "0.1.1" + "@atproto/oauth-types" "0.1.4" + "@atproto/xrpc" "0.6.1" + multiformats "^9.9.0" + zod "^3.23.8" + +"@atproto/oauth-types@0.1.4", "@atproto/oauth-types@^0.1.4": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@atproto/oauth-types/-/oauth-types-0.1.4.tgz#7ba79b4cc7acfb7c2125124fb84d24d4669804d8" + integrity sha512-B5lFXMvsx9PtO0wwCqwaoRVG8vKxvB742vO4Ze5OMJJsps6ebGskaYmkFHP9DnvDSLRzIHpJJ7jN6ri71V+xng== + dependencies: + "@atproto/jwk" "0.1.1" + zod "^3.23.8" + +"@atproto/syntax@0.3.0", "@atproto/syntax@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.3.0.tgz#fafa2dbea9add37253005cb663e7373e05e618b3" integrity sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA== -"@atproto/xrpc@^0.6.1": +"@atproto/xrpc@0.6.1", "@atproto/xrpc@^0.6.1": version "0.6.1" resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.1.tgz#dcd1315c8c60eef5af2db7fa4e35a38ebc6d79d5" integrity sha512-Zy5ydXEdk6sY7FDUZcEVfCL1jvbL4tXu5CcdPqbEaW6LQtk9GLds/DK1bCX9kswTGaBC88EMuqQMfkxOhp2t4A== @@ -49,14 +178,6 @@ "@atproto/lexicon" "^0.4.1" zod "^3.23.8" -"@babel/runtime-corejs3@^7.10.2": - version "7.20.6" - resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.20.6.tgz" - integrity sha512-tqeujPiuEfcH067mx+7otTQWROVMKHXEaOQcAeNV5dDdbPWvPcFA8/W9LXw2NfjNmOetqLl03dfnG2WALPlsRQ== - dependencies: - core-js-pure "^3.25.1" - regenerator-runtime "^0.13.11" - "@babel/runtime@^7.1.2": version "7.20.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd" @@ -64,13 +185,6 @@ dependencies: regenerator-runtime "^0.13.11" -"@babel/runtime@^7.10.2", "@babel/runtime@^7.18.9": - version "7.20.6" - resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz" - integrity sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA== - dependencies: - regenerator-runtime "^0.13.11" - "@babel/runtime@^7.13.10": version "7.22.3" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.3.tgz#0a7fce51d43adbf0f7b517a71f4c3aaca92ebcbb" @@ -129,14 +243,26 @@ debug "^3.1.0" lodash.once "^4.1.1" -"@eslint/eslintrc@^1.4.0": - version "1.4.0" - resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.0.tgz" - integrity sha512-7yfvXy6MWLgWSFsLhz5yH3iQ52St8cdUY6FoGieKkRDVxuxmrNuUetIuu6cmjNWwniUHiWXjxCr5tTXDrbYS5A== +"@eslint-community/eslint-utils@^4.2.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.6.1": + version "4.11.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.0.tgz#b0ffd0312b4a3fd2d6f77237e7248a5ad3a680ae" + integrity sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A== + +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.4.0" + espree "^9.6.0" globals "^13.19.0" ignore "^5.2.0" import-fresh "^3.2.1" @@ -144,6 +270,11 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@eslint/js@8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" + integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== + "@headlessui/react@^1.7.7": version "1.7.7" resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.7.tgz#d6f8708d8943ae8ebb1a6929108234e4515ac7e8" @@ -156,13 +287,13 @@ resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-2.0.13.tgz#9b1cc54ff77d6625c9565efdce0054a4bcd9074c" integrity sha512-iSN5XwmagrnirWlYEWNPdCDj9aRYVD/lnK3JlsC9/+fqGF80k8C7rl+1HCvBX0dBoagKqOFBs6fMhJJ1hOg1EQ== -"@humanwhocodes/config-array@^0.11.8": - version "0.11.8" - resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz" - integrity sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g== +"@humanwhocodes/config-array@^0.11.14": + version "0.11.14" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" + integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== dependencies: - "@humanwhocodes/object-schema" "^1.2.1" - debug "^4.1.1" + "@humanwhocodes/object-schema" "^2.0.2" + debug "^4.3.1" minimatch "^3.0.5" "@humanwhocodes/module-importer@^1.0.1": @@ -170,22 +301,22 @@ resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^1.2.1": - version "1.2.1" - resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@humanwhocodes/object-schema@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== "@next/env@14.2.5": version "14.2.5" resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.5.tgz#1d9328ab828711d3517d0a1d505acb55e5ef7ad0" integrity sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA== -"@next/eslint-plugin-next@13.4.8": - version "13.4.8" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-13.4.8.tgz#2aa7a0bbfc87fbed5aa0e938d0d16dca85061ee4" - integrity sha512-cmfVHpxWjjcETFt2WHnoFU6EmY69QcPJRlRNAooQlNe53Ke90vg1Ci/dkPffryJZaxxiRziP9bQrV8lDVCn3Fw== +"@next/eslint-plugin-next@14.2.5": + version "14.2.5" + resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.5.tgz#f7e3ff3efe40a2855e5f29bc2692175f85913ba8" + integrity sha512-LY3btOpPh+OTIpviNojDpUdIbHW9j0JBYBjsIp8IxtDFfYFyORvw3yNq6N231FVqQA7n7lwaf7xHbVJlA1ED7g== dependencies: - glob "7.1.7" + glob "10.3.10" "@next/swc-darwin-arm64@14.2.5": version "14.2.5" @@ -253,6 +384,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + "@pkgr/utils@^2.3.1": version "2.3.1" resolved "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz" @@ -301,10 +437,10 @@ resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2" integrity sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ== -"@rushstack/eslint-patch@^1.1.3": - version "1.2.0" - resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz" - integrity sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg== +"@rushstack/eslint-patch@^1.3.3": + version "1.10.4" + resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz#427d5549943a9c6fce808e39ea64dbe60d4047f1" + integrity sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA== "@swc/counter@^0.1.3": version "0.1.3" @@ -458,49 +594,51 @@ dependencies: "@types/node" "*" -"@typescript-eslint/parser@^5.42.0": - version "5.47.0" - resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.47.0.tgz" - integrity sha512-udPU4ckK+R1JWCGdQC4Qa27NtBg7w020ffHqGyAK8pAgOVuNw7YaKXGChk+udh+iiGIJf6/E/0xhVXyPAbsczw== +"@typescript-eslint/parser@^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.2.0.tgz#44356312aea8852a3a82deebdacd52ba614ec07a" + integrity sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg== dependencies: - "@typescript-eslint/scope-manager" "5.47.0" - "@typescript-eslint/types" "5.47.0" - "@typescript-eslint/typescript-estree" "5.47.0" + "@typescript-eslint/scope-manager" "7.2.0" + "@typescript-eslint/types" "7.2.0" + "@typescript-eslint/typescript-estree" "7.2.0" + "@typescript-eslint/visitor-keys" "7.2.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.47.0": - version "5.47.0" - resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.47.0.tgz" - integrity sha512-dvJab4bFf7JVvjPuh3sfBUWsiD73aiftKBpWSfi3sUkysDQ4W8x+ZcFpNp7Kgv0weldhpmMOZBjx1wKN8uWvAw== +"@typescript-eslint/scope-manager@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz#cfb437b09a84f95a0930a76b066e89e35d94e3da" + integrity sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg== dependencies: - "@typescript-eslint/types" "5.47.0" - "@typescript-eslint/visitor-keys" "5.47.0" + "@typescript-eslint/types" "7.2.0" + "@typescript-eslint/visitor-keys" "7.2.0" -"@typescript-eslint/types@5.47.0": - version "5.47.0" - resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.47.0.tgz" - integrity sha512-eslFG0Qy8wpGzDdYKu58CEr3WLkjwC5Usa6XbuV89ce/yN5RITLe1O8e+WFEuxnfftHiJImkkOBADj58ahRxSg== +"@typescript-eslint/types@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.2.0.tgz#0feb685f16de320e8520f13cca30779c8b7c403f" + integrity sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA== -"@typescript-eslint/typescript-estree@5.47.0": - version "5.47.0" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.47.0.tgz" - integrity sha512-LxfKCG4bsRGq60Sqqu+34QT5qT2TEAHvSCCJ321uBWywgE2dS0LKcu5u+3sMGo+Vy9UmLOhdTw5JHzePV/1y4Q== +"@typescript-eslint/typescript-estree@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz#5beda2876c4137f8440c5a84b4f0370828682556" + integrity sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA== dependencies: - "@typescript-eslint/types" "5.47.0" - "@typescript-eslint/visitor-keys" "5.47.0" + "@typescript-eslint/types" "7.2.0" + "@typescript-eslint/visitor-keys" "7.2.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" + minimatch "9.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" -"@typescript-eslint/visitor-keys@5.47.0": - version "5.47.0" - resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.47.0.tgz" - integrity sha512-ByPi5iMa6QqDXe/GmT/hR6MZtVPi0SqMQPDx15FczCBXJo/7M8T88xReOALAfpBLm+zxpPfmhuEvPb577JRAEg== +"@typescript-eslint/visitor-keys@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz#5035f177752538a5750cca1af6044b633610bf9e" + integrity sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A== dependencies: - "@typescript-eslint/types" "5.47.0" - eslint-visitor-keys "^3.3.0" + "@typescript-eslint/types" "7.2.0" + eslint-visitor-keys "^3.4.1" "@uiw/copy-to-clipboard@~1.0.12": version "1.0.15" @@ -535,6 +673,11 @@ rehype "~12.0.1" rehype-prism-plus "~1.6.1" +"@ungap/structured-clone@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + "@xobotyi/scrollbar-width@^1.9.5": version "1.9.5" resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d" @@ -564,10 +707,10 @@ acorn@^7.0.0: resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.8.0: - version "8.8.1" - resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz" - integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== +acorn@^8.9.0: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== aggregate-error@^3.0.0: version "3.1.0" @@ -577,7 +720,7 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" -ajv@^6.10.0, ajv@^6.12.4: +ajv@^6.12.4: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -634,15 +777,22 @@ argparse@^2.0.1: resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -aria-query@^4.2.2: - version "4.2.2" - resolved "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz" - integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== +aria-query@~5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" + integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== + dependencies: + deep-equal "^2.0.5" + +array-buffer-byte-length@^1.0.0, array-buffer-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" + integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== dependencies: - "@babel/runtime" "^7.10.2" - "@babel/runtime-corejs3" "^7.10.2" + call-bind "^1.0.5" + is-array-buffer "^3.0.4" -array-includes@^3.1.4, array-includes@^3.1.5, array-includes@^3.1.6: +array-includes@^3.1.5, array-includes@^3.1.6: version "3.1.6" resolved "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz" integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw== @@ -653,41 +803,91 @@ array-includes@^3.1.4, array-includes@^3.1.5, array-includes@^3.1.6: get-intrinsic "^1.1.3" is-string "^1.0.7" +array-includes@^3.1.7, array-includes@^3.1.8: + version "3.1.8" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" + integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + is-string "^1.0.7" + array-union@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array.prototype.flat@^1.2.5: - version "1.3.1" - resolved "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz" - integrity sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA== +array.prototype.findlast@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" + integrity sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-shim-unscopables "^1.0.2" + +array.prototype.findlastindex@^1.2.3: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz#8c35a755c72908719453f87145ca011e39334d0d" + integrity sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-shim-unscopables "^1.0.2" + +array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" + integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== dependencies: call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + define-properties "^1.2.0" + es-abstract "^1.22.1" es-shim-unscopables "^1.0.0" -array.prototype.flatmap@^1.3.1: - version "1.3.1" - resolved "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz" - integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ== +array.prototype.flatmap@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" + integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== dependencies: call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + define-properties "^1.2.0" + es-abstract "^1.22.1" es-shim-unscopables "^1.0.0" -array.prototype.tosorted@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz" - integrity sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ== +array.prototype.tosorted@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc" + integrity sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - es-shim-unscopables "^1.0.0" - get-intrinsic "^1.1.3" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.3" + es-errors "^1.3.0" + es-shim-unscopables "^1.0.2" + +arraybuffer.prototype.slice@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" + integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.2.1" + get-intrinsic "^1.2.3" + is-array-buffer "^3.0.4" + is-shared-array-buffer "^1.0.2" asap@~2.0.3: version "2.0.6" @@ -706,10 +906,10 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== -ast-types-flow@^0.0.7: - version "0.0.7" - resolved "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz" - integrity sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag== +ast-types-flow@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" + integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== astral-regex@^2.0.0: version "2.0.0" @@ -743,6 +943,13 @@ autoprefixer@^10.4.13: picocolors "^1.0.0" postcss-value-parser "^4.2.0" +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + await-lock@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.2.2.tgz#a95a9b269bfd2f69d22b17a321686f551152bcef" @@ -758,15 +965,17 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3" integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg== -axe-core@^4.4.3: - version "4.6.1" - resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.6.1.tgz" - integrity sha512-lCZN5XRuOnpG4bpMq8v0khrWtUOn+i8lZSb6wHZH56ZfbIEv6XwJV84AAueh9/zi7qPVJ/E4yz6fmsiyOmXR4w== +axe-core@^4.9.1: + version "4.10.0" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.0.tgz#d9e56ab0147278272739a000880196cdfe113b59" + integrity sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g== -axobject-query@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz" - integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA== +axobject-query@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1" + integrity sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg== + dependencies: + deep-equal "^2.0.5" bail@^2.0.0: version "2.0.2" @@ -828,6 +1037,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" @@ -878,6 +1094,17 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" @@ -995,6 +1222,15 @@ client-only@0.0.1, client-only@^0.0.1: resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + clsx@^1.1.1: version "1.2.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" @@ -1051,11 +1287,6 @@ copy-to-clipboard@^3.3.1: dependencies: toggle-selection "^1.0.6" -core-js-pure@^3.25.1: - version "3.26.1" - resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.26.1.tgz" - integrity sha512-VVXcDpp/xJ21KdULRq/lXdLzQAtX7+37LzpyfFM973il0tWSsDEoyzG38G14AjTpK9VTfiNM9jnFauq/CpaWGQ== - core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -1167,6 +1398,33 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +data-view-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" + integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" + integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" + integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + date-fns@^2.29.3: version "2.29.3" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" @@ -1177,13 +1435,6 @@ dayjs@^1.10.4: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.8.tgz#4282f139c8c19dd6d0c7bd571e30c2d0ba7698ea" integrity sha512-LcgxzFoWMEPO7ggRv1Y2N31hUf2R0Vj7fuy/m+Bg1K8rr+KAs1AEy4y9jd5DXe8pbHgX+srkHNS7TH6Q6ZhYeQ== -debug@^2.6.9: - version "2.6.9" - resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - debug@^3.1.0, debug@^3.2.7: version "3.2.7" resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" @@ -1198,6 +1449,13 @@ debug@^4.0.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: dependencies: ms "2.1.2" +debug@^4.3.1: + version "4.3.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" + integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== + dependencies: + ms "2.1.2" + decode-named-character-reference@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" @@ -1205,11 +1463,44 @@ decode-named-character-reference@^1.0.0: dependencies: character-entities "^2.0.0" +deep-equal@^2.0.5: + version "2.2.3" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1" + integrity sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA== + dependencies: + array-buffer-byte-length "^1.0.0" + call-bind "^1.0.5" + es-get-iterator "^1.1.3" + get-intrinsic "^1.2.2" + is-arguments "^1.1.1" + is-array-buffer "^3.0.2" + is-date-object "^1.0.5" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + isarray "^2.0.5" + object-is "^1.1.5" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.5.1" + side-channel "^1.0.4" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.13" + deep-is@^0.1.3: version "0.1.4" resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + define-lazy-prop@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz" @@ -1223,6 +1514,15 @@ define-properties@^1.1.3, define-properties@^1.1.4: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +define-properties@^1.2.0, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + defined@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz" @@ -1340,6 +1640,58 @@ error-stack-parser@^2.0.6: dependencies: stackframe "^1.3.4" +es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: + version "1.23.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" + integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== + dependencies: + array-buffer-byte-length "^1.0.1" + arraybuffer.prototype.slice "^1.0.3" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + data-view-buffer "^1.0.1" + data-view-byte-length "^1.0.1" + data-view-byte-offset "^1.0.0" + es-define-property "^1.0.0" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-set-tostringtag "^2.0.3" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.4" + get-symbol-description "^1.0.2" + globalthis "^1.0.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + hasown "^2.0.2" + internal-slot "^1.0.7" + is-array-buffer "^3.0.4" + is-callable "^1.2.7" + is-data-view "^1.0.1" + is-negative-zero "^2.0.3" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.3" + is-string "^1.0.7" + is-typed-array "^1.1.13" + is-weakref "^1.0.2" + object-inspect "^1.13.1" + object-keys "^1.1.1" + object.assign "^4.1.5" + regexp.prototype.flags "^1.5.2" + safe-array-concat "^1.1.2" + safe-regex-test "^1.0.3" + string.prototype.trim "^1.2.9" + string.prototype.trimend "^1.0.8" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.2" + typed-array-byte-length "^1.0.1" + typed-array-byte-offset "^1.0.2" + typed-array-length "^1.0.6" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.15" + es-abstract@^1.19.0, es-abstract@^1.20.4: version "1.20.5" resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.5.tgz" @@ -1371,6 +1723,69 @@ es-abstract@^1.19.0, es-abstract@^1.20.4: string.prototype.trimstart "^1.0.6" unbox-primitive "^1.0.2" +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.2.1, es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-get-iterator@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" + integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + has-symbols "^1.0.3" + is-arguments "^1.1.1" + is-map "^2.0.2" + is-set "^2.0.2" + is-string "^1.0.7" + isarray "^2.0.5" + stop-iteration-iterator "^1.0.0" + +es-iterator-helpers@^1.0.19: + version "1.0.19" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz#117003d0e5fec237b4b5c08aded722e0c6d50ca8" + integrity sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.3" + es-errors "^1.3.0" + es-set-tostringtag "^2.0.3" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + globalthis "^1.0.3" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + internal-slot "^1.0.7" + iterator.prototype "^1.1.2" + safe-array-concat "^1.1.2" + +es-object-atoms@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" + integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" + integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== + dependencies: + get-intrinsic "^1.2.4" + has-tostringtag "^1.0.2" + hasown "^2.0.1" + es-shim-unscopables@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz" @@ -1378,6 +1793,13 @@ es-shim-unscopables@^1.0.0: dependencies: has "^1.0.3" +es-shim-unscopables@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" + integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== + dependencies: + hasown "^2.0.0" + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz" @@ -1407,20 +1829,20 @@ escape-string-regexp@^5.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== -eslint-config-next@13.4.8: - version "13.4.8" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-13.4.8.tgz#f2683d278ae72f7cf8854d571b05cce3bfd84143" - integrity sha512-2hE0b6lHuhtHBX8VgEXi8v4G8PVrPUBMOSLCTq8qtcQ2qQOX7+uBOLK2kU4FD2qDZzyXNlhmuH+WLT5ptY4XLA== +eslint-config-next@^14.2.4: + version "14.2.5" + resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-14.2.5.tgz#cdd43d89047eb7391ba25445d5855b4600b6adb9" + integrity sha512-zogs9zlOiZ7ka+wgUnmcM0KBEDjo4Jis7kxN1jvC0N4wynQ2MIx/KBkg4mVF63J5EK4W0QMCn7xO3vNisjaAoA== dependencies: - "@next/eslint-plugin-next" "13.4.8" - "@rushstack/eslint-patch" "^1.1.3" - "@typescript-eslint/parser" "^5.42.0" + "@next/eslint-plugin-next" "14.2.5" + "@rushstack/eslint-patch" "^1.3.3" + "@typescript-eslint/parser" "^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0" eslint-import-resolver-node "^0.3.6" eslint-import-resolver-typescript "^3.5.2" - eslint-plugin-import "^2.26.0" - eslint-plugin-jsx-a11y "^6.5.1" - eslint-plugin-react "^7.31.7" - eslint-plugin-react-hooks "^4.5.0" + eslint-plugin-import "^2.28.1" + eslint-plugin-jsx-a11y "^6.7.1" + eslint-plugin-react "^7.33.2" + eslint-plugin-react-hooks "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" eslint-import-resolver-node@^0.3.6: version "0.3.6" @@ -1430,6 +1852,15 @@ eslint-import-resolver-node@^0.3.6: debug "^3.2.7" resolve "^1.20.0" +eslint-import-resolver-node@^0.3.9: + version "0.3.9" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" + integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== + dependencies: + debug "^3.2.7" + is-core-module "^2.13.0" + resolve "^1.22.4" + eslint-import-resolver-typescript@^3.5.2: version "3.5.2" resolved "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.2.tgz" @@ -1443,160 +1874,162 @@ eslint-import-resolver-typescript@^3.5.2: is-glob "^4.0.3" synckit "^0.8.4" -eslint-module-utils@^2.7.3: - version "2.7.4" - resolved "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz" - integrity sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA== +eslint-module-utils@^2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz#52f2404300c3bd33deece9d7372fb337cc1d7c34" + integrity sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q== dependencies: debug "^3.2.7" -eslint-plugin-import@^2.26.0: - version "2.26.0" - resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz" - integrity sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA== +eslint-plugin-import@^2.28.1: + version "2.29.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz#d45b37b5ef5901d639c15270d74d46d161150643" + integrity sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw== dependencies: - array-includes "^3.1.4" - array.prototype.flat "^1.2.5" - debug "^2.6.9" + array-includes "^3.1.7" + array.prototype.findlastindex "^1.2.3" + array.prototype.flat "^1.3.2" + array.prototype.flatmap "^1.3.2" + debug "^3.2.7" doctrine "^2.1.0" - eslint-import-resolver-node "^0.3.6" - eslint-module-utils "^2.7.3" - has "^1.0.3" - is-core-module "^2.8.1" + eslint-import-resolver-node "^0.3.9" + eslint-module-utils "^2.8.0" + hasown "^2.0.0" + is-core-module "^2.13.1" is-glob "^4.0.3" minimatch "^3.1.2" - object.values "^1.1.5" - resolve "^1.22.0" - tsconfig-paths "^3.14.1" - -eslint-plugin-jsx-a11y@^6.5.1: - version "6.6.1" - resolved "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz" - integrity sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q== - dependencies: - "@babel/runtime" "^7.18.9" - aria-query "^4.2.2" - array-includes "^3.1.5" - ast-types-flow "^0.0.7" - axe-core "^4.4.3" - axobject-query "^2.2.0" + object.fromentries "^2.0.7" + object.groupby "^1.0.1" + object.values "^1.1.7" + semver "^6.3.1" + tsconfig-paths "^3.15.0" + +eslint-plugin-jsx-a11y@^6.7.1: + version "6.9.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.9.0.tgz#67ab8ff460d4d3d6a0b4a570e9c1670a0a8245c8" + integrity sha512-nOFOCaJG2pYqORjK19lqPqxMO/JpvdCZdPtNdxY3kvom3jTvkAbOvQvD8wuD0G8BYR0IGAGYDlzqWJOh/ybn2g== + dependencies: + aria-query "~5.1.3" + array-includes "^3.1.8" + array.prototype.flatmap "^1.3.2" + ast-types-flow "^0.0.8" + axe-core "^4.9.1" + axobject-query "~3.1.1" damerau-levenshtein "^1.0.8" emoji-regex "^9.2.2" - has "^1.0.3" - jsx-ast-utils "^3.3.2" - language-tags "^1.0.5" + es-iterator-helpers "^1.0.19" + hasown "^2.0.2" + jsx-ast-utils "^3.3.5" + language-tags "^1.0.9" minimatch "^3.1.2" - semver "^6.3.0" - -eslint-plugin-react-hooks@^4.5.0: - version "4.6.0" - resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz" - integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== + object.fromentries "^2.0.8" + safe-regex-test "^1.0.3" + string.prototype.includes "^2.0.0" -eslint-plugin-react@^7.31.7: - version "7.31.11" - resolved "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.11.tgz" - integrity sha512-TTvq5JsT5v56wPa9OYHzsrOlHzKZKjV+aLgS+55NJP/cuzdiQPC7PfYoUjMoxlffKtvijpk7vA/jmuqRb9nohw== - dependencies: - array-includes "^3.1.6" - array.prototype.flatmap "^1.3.1" - array.prototype.tosorted "^1.1.1" +"eslint-plugin-react-hooks@^4.5.0 || 5.0.0-canary-7118f5dd7-20230705": + version "4.6.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596" + integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== + +eslint-plugin-react@^7.33.2: + version "7.35.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz#00b1e4559896710e58af6358898f2ff917ea4c41" + integrity sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA== + dependencies: + array-includes "^3.1.8" + array.prototype.findlast "^1.2.5" + array.prototype.flatmap "^1.3.2" + array.prototype.tosorted "^1.1.4" doctrine "^2.1.0" + es-iterator-helpers "^1.0.19" estraverse "^5.3.0" + hasown "^2.0.2" jsx-ast-utils "^2.4.1 || ^3.0.0" minimatch "^3.1.2" - object.entries "^1.1.6" - object.fromentries "^2.0.6" - object.hasown "^1.1.2" - object.values "^1.1.6" + object.entries "^1.1.8" + object.fromentries "^2.0.8" + object.values "^1.2.0" prop-types "^15.8.1" - resolve "^2.0.0-next.3" - semver "^6.3.0" - string.prototype.matchall "^4.0.8" + resolve "^2.0.0-next.5" + semver "^6.3.1" + string.prototype.matchall "^4.0.11" + string.prototype.repeat "^1.0.0" -eslint-scope@^7.1.1: - version "7.1.1" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz" - integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== - dependencies: - eslint-visitor-keys "^2.0.0" - -eslint-visitor-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz" - integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== - eslint-visitor-keys@^3.3.0: version "3.3.0" resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@8.30.0: - version "8.30.0" - resolved "https://registry.npmjs.org/eslint/-/eslint-8.30.0.tgz" - integrity sha512-MGADB39QqYuzEGov+F/qb18r4i7DohCDOfatHaxI2iGlPuC65bwG2gxgO+7DkyL38dRFaRH7RaRAgU6JKL9rMQ== - dependencies: - "@eslint/eslintrc" "^1.4.0" - "@humanwhocodes/config-array" "^0.11.8" +eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint@^8.57.0: + version "8.57.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" + integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.57.0" + "@humanwhocodes/config-array" "^0.11.14" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" - ajv "^6.10.0" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" chalk "^4.0.0" cross-spawn "^7.0.2" debug "^4.3.2" doctrine "^3.0.0" escape-string-regexp "^4.0.0" - eslint-scope "^7.1.1" - eslint-utils "^3.0.0" - eslint-visitor-keys "^3.3.0" - espree "^9.4.0" - esquery "^1.4.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" file-entry-cache "^6.0.1" find-up "^5.0.0" glob-parent "^6.0.2" globals "^13.19.0" - grapheme-splitter "^1.0.4" + graphemer "^1.4.0" ignore "^5.2.0" - import-fresh "^3.0.0" imurmurhash "^0.1.4" is-glob "^4.0.0" is-path-inside "^3.0.3" - js-sdsl "^4.1.4" js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" lodash.merge "^4.6.2" minimatch "^3.1.2" natural-compare "^1.4.0" - optionator "^0.9.1" - regexpp "^3.2.0" + optionator "^0.9.3" strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" text-table "^0.2.0" -espree@^9.4.0: - version "9.4.1" - resolved "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz" - integrity sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg== +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== dependencies: - acorn "^8.8.0" + acorn "^8.9.0" acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.3.0" + eslint-visitor-keys "^3.4.1" -esquery@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz" - integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== +esquery@^1.4.2: + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== dependencies: estraverse "^5.1.0" @@ -1805,6 +2238,21 @@ flux@^4.0.1: fbemitter "^3.0.0" fbjs "^3.0.1" +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + +foreground-child@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" + integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -1849,6 +2297,11 @@ function-bind@^1.1.1: resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + function.prototype.name@^1.1.5: version "1.1.5" resolved "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz" @@ -1859,7 +2312,17 @@ function.prototype.name@^1.1.5: es-abstract "^1.19.0" functions-have-names "^1.2.2" -functions-have-names@^1.2.2: +function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" + +functions-have-names@^1.2.2, functions-have-names@^1.2.3: version "1.2.3" resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== @@ -1878,6 +2341,17 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: has "^1.0.3" has-symbols "^1.0.3" +get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + get-stream@^5.0.0, get-stream@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" @@ -1893,6 +2367,15 @@ get-symbol-description@^1.0.0: call-bind "^1.0.2" get-intrinsic "^1.1.1" +get-symbol-description@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" + integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== + dependencies: + call-bind "^1.0.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + get-tsconfig@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.2.0.tgz" @@ -1931,17 +2414,16 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@7.1.7: - version "7.1.7" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== +glob@10.3.10: + version "10.3.10" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" + integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" + foreground-child "^3.1.0" + jackspeak "^2.3.5" + minimatch "^9.0.1" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-scurry "^1.10.1" glob@^7.1.3: version "7.2.3" @@ -1969,6 +2451,14 @@ globals@^13.19.0: dependencies: type-fest "^0.20.2" +globalthis@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== + dependencies: + define-properties "^1.2.1" + gopd "^1.0.1" + globalyzer@0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz" @@ -2019,11 +2509,6 @@ graceful-fs@^4.2.4: resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -grapheme-splitter@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz" - integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== - graphemer@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" @@ -2046,6 +2531,18 @@ has-property-descriptors@^1.0.0: dependencies: get-intrinsic "^1.1.1" +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1, has-proto@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" @@ -2058,6 +2555,13 @@ has-tostringtag@^1.0.0: dependencies: has-symbols "^1.0.2" +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + has@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" @@ -2065,6 +2569,13 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + hast-util-from-parse5@^7.0.0: version "7.1.2" resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-7.1.2.tgz#aecfef73e3ceafdfa4550716443e4eb7b02e22b0" @@ -2241,7 +2752,7 @@ ignore@^5.2.0: resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== -import-fresh@^3.0.0, import-fresh@^3.2.1: +import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -2299,6 +2810,15 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +internal-slot@^1.0.4, internal-slot@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" + integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.0" + side-channel "^1.0.4" + is-alphabetical@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" @@ -2312,6 +2832,29 @@ is-alphanumerical@^2.0.0: is-alphabetical "^2.0.0" is-decimal "^2.0.0" +is-arguments@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-array-buffer@^3.0.2, is-array-buffer@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" + integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + +is-async-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.0.0.tgz#8e4418efd3e5d3a6ebb0164c05ef5afb69aa9646" + integrity sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA== + dependencies: + has-tostringtag "^1.0.0" + is-bigint@^1.0.1: version "1.0.4" resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz" @@ -2339,7 +2882,7 @@ is-buffer@^2.0.0: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== -is-callable@^1.1.4, is-callable@^1.2.7: +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: version "1.2.7" resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== @@ -2351,14 +2894,28 @@ is-ci@^3.0.0: dependencies: ci-info "^3.2.0" -is-core-module@^2.10.0, is-core-module@^2.8.1, is-core-module@^2.9.0: +is-core-module@^2.10.0, is-core-module@^2.9.0: version "2.11.0" resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz" integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== dependencies: has "^1.0.3" -is-date-object@^1.0.1: +is-core-module@^2.13.0, is-core-module@^2.13.1: + version "2.15.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.0.tgz#71c72ec5442ace7e76b306e9d48db361f22699ea" + integrity sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA== + dependencies: + hasown "^2.0.2" + +is-data-view@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" + integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== + dependencies: + is-typed-array "^1.1.13" + +is-date-object@^1.0.1, is-date-object@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz" integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== @@ -2380,11 +2937,25 @@ is-extglob@^2.1.1: resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== +is-finalizationregistry@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz#c8749b65f17c133313e661b1289b95ad3dbd62e6" + integrity sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== + dependencies: + call-bind "^1.0.2" + is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +is-generator-function@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" @@ -2405,11 +2976,21 @@ is-installed-globally@~0.4.0: global-dirs "^3.0.0" is-path-inside "^3.0.2" +is-map@^2.0.2, is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + is-negative-zero@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz" integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== + is-number-object@^1.0.4: version "1.0.7" resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz" @@ -2440,6 +3021,11 @@ is-regex@^1.1.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-set@^2.0.2, is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + is-shared-array-buffer@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz" @@ -2447,6 +3033,13 @@ is-shared-array-buffer@^1.0.2: dependencies: call-bind "^1.0.2" +is-shared-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" + integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== + dependencies: + call-bind "^1.0.7" + is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" @@ -2466,6 +3059,13 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" +is-typed-array@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== + dependencies: + which-typed-array "^1.1.14" + is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -2476,6 +3076,11 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +is-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== + is-weakref@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz" @@ -2483,6 +3088,14 @@ is-weakref@^1.0.2: dependencies: call-bind "^1.0.2" +is-weakset@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.3.tgz#e801519df8c0c43e12ff2834eead84ec9e624007" + integrity sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz" @@ -2490,6 +3103,11 @@ is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" @@ -2505,16 +3123,36 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== +iterator.prototype@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" + integrity sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w== + dependencies: + define-properties "^1.2.1" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + reflect.getprototypeof "^1.0.4" + set-function-name "^2.0.1" + +jackspeak@2.1.1, jackspeak@^2.3.5: + version "2.1.1" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.1.1.tgz#2a42db4cfbb7e55433c28b6f75d8b796af9669cd" + integrity sha512-juf9stUEwUaILepraGOWIJTLwg48bUnBmRqd2ln2Os1sW987zeoj/hzhbvRB95oMuS2ZTpjULmdwHNX4rzZIZw== + dependencies: + cliui "^8.0.1" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +jose@^5.2.0: + version "5.6.3" + resolved "https://registry.yarnpkg.com/jose/-/jose-5.6.3.tgz#415688bc84875461c86dfe271ea6029112a23e27" + integrity sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g== + js-cookie@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ== -js-sdsl@^4.1.4: - version "4.2.0" - resolved "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz" - integrity sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ== - "js-tokens@^3.0.0 || ^4.0.0": version "4.0.0" resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" @@ -2552,10 +3190,10 @@ json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== -json5@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== +json5@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: minimist "^1.2.0" @@ -2578,7 +3216,7 @@ jsprim@^2.0.2: json-schema "0.4.0" verror "1.10.0" -"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.2: +"jsx-ast-utils@^2.4.1 || ^3.0.0": version "3.3.3" resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz" integrity sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw== @@ -2586,6 +3224,16 @@ jsprim@^2.0.2: array-includes "^3.1.5" object.assign "^4.1.3" +jsx-ast-utils@^3.3.5: + version "3.3.5" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" + integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== + dependencies: + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + object.assign "^4.1.4" + object.values "^1.1.6" + kbar@^0.1.0-beta.45: version "0.1.0-beta.45" resolved "https://registry.yarnpkg.com/kbar/-/kbar-0.1.0-beta.45.tgz#6b0871f2860a7fe21ad4db5df2389cf3b73d344a" @@ -2619,10 +3267,10 @@ language-subtag-registry@^0.3.20: resolved "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz" integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w== -language-tags@^1.0.5: - version "1.0.7" - resolved "https://registry.npmjs.org/language-tags/-/language-tags-1.0.7.tgz" - integrity sha512-bSytju1/657hFjgUzPAPqszxH62ouE8nQFoFaVlIQfne4wO/wXC9A4+m8jYve7YBBvi59eq0SUpcshvG8h5Usw== +language-tags@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.9.tgz#1ffdcd0ec0fafb4b1be7f8b11f306ad0f9c08777" + integrity sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA== dependencies: language-subtag-registry "^0.3.20" @@ -2730,6 +3378,11 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" @@ -3198,13 +3851,27 @@ mini-svg-data-uri@^1.2.3: resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz#8ab0aabcdf8c29ad5693ca595af19dd2ead09939" integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg== -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" +minimatch@^9.0.1: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0, minimist@^1.2.6: version "1.2.7" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz" @@ -3215,16 +3882,16 @@ minimist@^1.2.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + mri@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== -ms@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== - ms@2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" @@ -3348,6 +4015,19 @@ object-inspect@^1.12.2, object-inspect@^1.9.0: resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz" integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + +object-is@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" + integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + object-keys@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" @@ -3363,33 +4043,45 @@ object.assign@^4.1.3, object.assign@^4.1.4: has-symbols "^1.0.3" object-keys "^1.1.1" -object.entries@^1.1.6: - version "1.1.6" - resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz" - integrity sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w== +object.assign@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + call-bind "^1.0.5" + define-properties "^1.2.1" + has-symbols "^1.0.3" + object-keys "^1.1.1" -object.fromentries@^2.0.6: - version "2.0.6" - resolved "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz" - integrity sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg== +object.entries@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" + integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" -object.hasown@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz" - integrity sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw== +object.fromentries@^2.0.7, object.fromentries@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" + integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== dependencies: - define-properties "^1.1.4" - es-abstract "^1.20.4" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + +object.groupby@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e" + integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" -object.values@^1.1.5, object.values@^1.1.6: +object.values@^1.1.6: version "1.1.6" resolved "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz" integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw== @@ -3398,6 +4090,15 @@ object.values@^1.1.5, object.values@^1.1.6: define-properties "^1.1.4" es-abstract "^1.20.4" +object.values@^1.1.7, object.values@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" + integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" @@ -3421,17 +4122,17 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== dependencies: deep-is "^0.1.3" fast-levenshtein "^2.0.6" levn "^0.4.1" prelude-ls "^1.2.1" type-check "^0.4.0" - word-wrap "^1.2.3" + word-wrap "^1.2.5" ospath@^1.2.2: version "1.2.2" @@ -3510,6 +4211,14 @@ path-parse@^1.0.7: resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-scurry@^1.10.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-type@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" @@ -3540,6 +4249,11 @@ pify@^2.2.0, pify@^2.3.0: resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + postcss-import@^14.1.0: version "14.1.0" resolved "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz" @@ -3831,6 +4545,19 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +reflect.getprototypeof@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" + integrity sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.1" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + globalthis "^1.0.3" + which-builtin-type "^1.1.3" + refractor@^4.8.0: version "4.8.1" resolved "https://registry.yarnpkg.com/refractor/-/refractor-4.8.1.tgz#fbdd889333a3d86c9c864479622855c9b38e9d42" @@ -3860,10 +4587,15 @@ regexp.prototype.flags@^1.4.3: define-properties "^1.1.3" functions-have-names "^1.2.2" -regexpp@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz" - integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== +regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" + integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== + dependencies: + call-bind "^1.0.6" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.1" rehype-attr@~2.1.0: version "2.1.4" @@ -4055,7 +4787,7 @@ resolve-from@^4.0.0: resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve@^1.1.7, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.22.1: +resolve@^1.1.7, resolve@^1.20.0, resolve@^1.22.1: version "1.22.1" resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz" integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== @@ -4064,12 +4796,21 @@ resolve@^1.1.7, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.22.1: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^2.0.0-next.3: - version "2.0.0-next.4" - resolved "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz" - integrity sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ== +resolve@^1.22.4: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== dependencies: - is-core-module "^2.9.0" + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +resolve@^2.0.0-next.5: + version "2.0.0-next.5" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" + integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== + dependencies: + is-core-module "^2.13.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -4126,6 +4867,16 @@ sade@^1.7.3: dependencies: mri "^1.1.0" +safe-array-concat@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" + integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + has-symbols "^1.0.3" + isarray "^2.0.5" + safe-buffer@^5.0.1, safe-buffer@^5.1.2: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -4140,6 +4891,15 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" +safe-regex-test@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" + integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-regex "^1.1.4" + safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -4157,10 +4917,10 @@ screenfull@^5.1.0: resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.2.0.tgz#6533d524d30621fc1283b9692146f3f13a93d1ba" integrity sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA== -semver@^6.3.0: - version "6.3.0" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== semver@^7.3.2: version "7.5.1" @@ -4169,12 +4929,32 @@ semver@^7.3.2: dependencies: lru-cache "^6.0.0" -semver@^7.3.7: - version "7.3.8" - resolved "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz" - integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== +semver@^7.5.4: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== dependencies: - lru-cache "^6.0.0" + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +set-function-name@^2.0.1, set-function-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" set-harmonic-interval@^1.0.1: version "1.0.1" @@ -4207,11 +4987,26 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" +side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + signal-exit@^3.0.2: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + slash@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" @@ -4309,6 +5104,13 @@ stacktrace-js@^2.0.2: stack-generator "^2.0.5" stacktrace-gps "^3.0.4" +stop-iteration-iterator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" + integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== + dependencies: + internal-slot "^1.0.4" + streamsearch@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" @@ -4323,19 +5125,49 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string.prototype.matchall@^4.0.8: - version "4.0.8" - resolved "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz" - integrity sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg== +string.prototype.includes@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string.prototype.includes/-/string.prototype.includes-2.0.0.tgz#8986d57aee66d5460c144620a6d873778ad7289f" + integrity sha512-E34CkBgyeqNDcrbU76cDjL5JLcVrtSdYq0MEh/B10r17pRP4ciHLwTgnuLV8Ay6cgEMLkcBkFCKyFZ43YldYzg== dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - get-intrinsic "^1.1.3" + define-properties "^1.1.3" + es-abstract "^1.17.5" + +string.prototype.matchall@^4.0.11: + version "4.0.11" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" + integrity sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + gopd "^1.0.1" has-symbols "^1.0.3" - internal-slot "^1.0.3" - regexp.prototype.flags "^1.4.3" - side-channel "^1.0.4" + internal-slot "^1.0.7" + regexp.prototype.flags "^1.5.2" + set-function-name "^2.0.2" + side-channel "^1.0.6" + +string.prototype.repeat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz#e90872ee0308b29435aa26275f6e1b762daee01a" + integrity sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + +string.prototype.trim@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" + integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-object-atoms "^1.0.0" string.prototype.trimend@^1.0.6: version "1.0.6" @@ -4346,6 +5178,15 @@ string.prototype.trimend@^1.0.6: define-properties "^1.1.4" es-abstract "^1.20.4" +string.prototype.trimend@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" + integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + string.prototype.trimstart@^1.0.6: version "1.0.6" resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz" @@ -4355,6 +5196,15 @@ string.prototype.trimstart@^1.0.6: define-properties "^1.1.4" es-abstract "^1.20.4" +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + stringify-entities@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.3.tgz#cfabd7039d22ad30f3cc435b0ca2c1574fc88ef8" @@ -4380,7 +5230,7 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -4550,26 +5400,26 @@ trough@^2.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-2.1.0.tgz#0f7b511a4fde65a46f18477ab38849b22c554876" integrity sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g== +ts-api-utils@^1.0.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" + integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== + ts-easing@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec" integrity sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ== -tsconfig-paths@^3.14.1: - version "3.14.1" - resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz" - integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== +tsconfig-paths@^3.15.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" + integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== dependencies: "@types/json5" "^0.0.29" - json5 "^1.0.1" + json5 "^1.0.2" minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^1.8.1: - version "1.14.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - tslib@^2.1.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" @@ -4580,13 +5430,6 @@ tslib@^2.4.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== -tsutils@^3.21.0: - version "3.21.0" - resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" - integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== - dependencies: - tslib "^1.8.1" - tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" @@ -4616,10 +5459,54 @@ type-fest@^0.21.3: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== -typescript@4.9.4: - version "4.9.4" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz" - integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== +typed-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" + integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-typed-array "^1.1.13" + +typed-array-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" + integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-byte-offset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" + integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-length@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" + integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + +typescript@^5.4.5: + version "5.5.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" + integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== ua-parser-js@^1.0.35: version "1.0.37" @@ -4839,6 +5726,45 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" +which-builtin-type@^1.1.3: + version "1.1.4" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.4.tgz#592796260602fc3514a1b5ee7fa29319b72380c3" + integrity sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w== + dependencies: + function.prototype.name "^1.1.6" + has-tostringtag "^1.0.2" + is-async-function "^2.0.0" + is-date-object "^1.0.5" + is-finalizationregistry "^1.0.2" + is-generator-function "^1.0.10" + is-regex "^1.1.4" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.2" + which-typed-array "^1.1.15" + +which-collection@^1.0.1, which-collection@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== + dependencies: + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" + +which-typed-array@^1.1.13, which-typed-array@^1.1.14, which-typed-array@^1.1.15: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" + which@^2.0.1: version "2.0.2" resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" @@ -4846,10 +5772,10 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -word-wrap@^1.2.3: - version "1.2.3" - resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== wrap-ansi@^6.2.0: version "6.2.0"