From d678eb6b9a785494f369a10d7d9694565b4bbb55 Mon Sep 17 00:00:00 2001 From: tarashagarwal Date: Mon, 17 Feb 2025 22:11:03 -0600 Subject: [PATCH 01/13] Adding Siglead Management Option in the Side Menu & Adding a draft Page --- src/common/roles.ts | 1 + src/ui/Router.tsx | 5 + src/ui/components/AppShell/index.tsx | 15 +- src/ui/pages/siglead/ManageSigLeads.page.tsx | 207 +++++++++++++++++++ 4 files changed, 221 insertions(+), 7 deletions(-) create mode 100644 src/ui/pages/siglead/ManageSigLeads.page.tsx diff --git a/src/common/roles.ts b/src/common/roles.ts index c61d572c..a713b930 100644 --- a/src/common/roles.ts +++ b/src/common/roles.ts @@ -3,6 +3,7 @@ export const runEnvironments = ["dev", "prod"] as const; export type RunEnvironment = (typeof runEnvironments)[number]; export enum AppRoles { EVENTS_MANAGER = "manage:events", + SIGLEAD_MANAGER = "manage:siglead", TICKETS_SCANNER = "scan:tickets", TICKETS_MANAGER = "manage:tickets", IAM_ADMIN = "admin:iam", diff --git a/src/ui/Router.tsx b/src/ui/Router.tsx index 5e8528e3..44654cfb 100644 --- a/src/ui/Router.tsx +++ b/src/ui/Router.tsx @@ -19,6 +19,7 @@ import { ViewTicketsPage } from './pages/tickets/ViewTickets.page'; import { ManageIamPage } from './pages/iam/ManageIam.page'; import { ManageProfilePage } from './pages/profile/ManageProfile.page'; import { ManageStripeLinksPage } from './pages/stripe/ViewLinks.page'; +import { ManageSigLeadsPage } from './pages/siglead/ManageSigLeads.page'; const ProfileRediect: React.FC = () => { const location = useLocation(); @@ -162,6 +163,10 @@ const authenticatedRouter = createBrowserRouter([ path: '/stripe', element: , }, + { + path: '/siglead-management', + element: , + }, // Catch-all route for authenticated users shows 404 page { path: '*', diff --git a/src/ui/components/AppShell/index.tsx b/src/ui/components/AppShell/index.tsx index 8b5183ab..9697d673 100644 --- a/src/ui/components/AppShell/index.tsx +++ b/src/ui/components/AppShell/index.tsx @@ -17,6 +17,7 @@ import { IconPizza, IconTicket, IconLock, + IconUsers, } from '@tabler/icons-react'; import { ReactNode } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -37,13 +38,6 @@ export interface AcmAppShellProps { } export const navItems = [ - { - link: '/events/manage', - name: 'Events', - icon: IconCalendar, - description: null, - validRoles: [AppRoles.EVENTS_MANAGER], - }, { link: '/tickets', name: 'Ticketing/Merch', @@ -65,6 +59,13 @@ export const navItems = [ description: null, validRoles: [AppRoles.STRIPE_LINK_CREATOR], }, + { + link: '/siglead-management', + name: 'SigLead', + icon: IconUsers, + description: null, + validRoles: [AppRoles.SIGLEAD_MANAGER], + }, ]; export const extLinks = [ diff --git a/src/ui/pages/siglead/ManageSigLeads.page.tsx b/src/ui/pages/siglead/ManageSigLeads.page.tsx new file mode 100644 index 00000000..521e8885 --- /dev/null +++ b/src/ui/pages/siglead/ManageSigLeads.page.tsx @@ -0,0 +1,207 @@ +import { Title, Box, TextInput, Textarea, Switch, Select, Button, Loader } from '@mantine/core'; +import { DateTimePicker } from '@mantine/dates'; +import { useForm, zodResolver } from '@mantine/form'; +import { notifications } from '@mantine/notifications'; +import dayjs from 'dayjs'; +import React, { useEffect, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { z } from 'zod'; +import { AuthGuard } from '@ui/components/AuthGuard'; +import { getRunEnvironmentConfig } from '@ui/config'; +import { useApi } from '@ui/util/api'; +import { OrganizationList as orgList } from '@common/orgs'; +import { AppRoles } from '@common/roles'; + +export function capitalizeFirstLetter(string: string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} + +const repeatOptions = ['weekly', 'biweekly'] as const; + +const baseBodySchema = z.object({ + title: z.string().min(1, 'Title is required'), + description: z.string().min(1, 'Description is required'), + start: z.date(), + end: z.optional(z.date()), + location: z.string().min(1, 'Location is required'), + locationLink: z.optional(z.string().url('http://github.com/Invalid%20URL')), + host: z.string().min(1, 'Host is required'), + featured: z.boolean().default(false), + paidEventId: z.string().min(1, 'Paid Event ID must be at least 1 character').optional(), +}); + +const requestBodySchema = baseBodySchema + .extend({ + repeats: z.optional(z.enum(repeatOptions)).nullable(), + repeatEnds: z.date().optional(), + }) + .refine((data) => (data.repeatEnds ? data.repeats !== undefined : true), { + message: 'Repeat frequency is required when Repeat End is specified.', + }) + .refine((data) => !data.end || data.end >= data.start, { + message: 'Event end date cannot be earlier than the start date.', + path: ['end'], + }) + .refine((data) => !data.repeatEnds || data.repeatEnds >= data.start, { + message: 'Repeat end date cannot be earlier than the start date.', + path: ['repeatEnds'], + }); + +type EventPostRequest = z.infer; + +export const ManageSigLeadsPage: React.FC = () => { + const [isSubmitting, setIsSubmitting] = useState(false); + const navigate = useNavigate(); + const api = useApi('core'); + + const { eventId } = useParams(); + + const isEditing = eventId !== undefined; + + useEffect(() => { + if (!isEditing) { + return; + } + // Fetch event data and populate form + const getEvent = async () => { + try { + const response = await api.get(`/api/v1/events/${eventId}`); + const eventData = response.data; + const formValues = { + title: eventData.title, + description: eventData.description, + start: new Date(eventData.start), + end: eventData.end ? new Date(eventData.end) : undefined, + location: eventData.location, + locationLink: eventData.locationLink, + host: eventData.host, + featured: eventData.featured, + repeats: eventData.repeats, + repeatEnds: eventData.repeatEnds ? new Date(eventData.repeatEnds) : undefined, + paidEventId: eventData.paidEventId, + }; + form.setValues(formValues); + } catch (error) { + console.error('Error fetching event data:', error); + notifications.show({ + message: 'Failed to fetch event data, please try again.', + }); + } + }; + getEvent(); + }, [eventId, isEditing]); + + const form = useForm({ + validate: zodResolver(requestBodySchema), + initialValues: { + title: '', + description: '', + start: new Date(), + end: new Date(new Date().valueOf() + 3.6e6), // 1 hr later + location: 'ACM Room (Siebel CS 1104)', + locationLink: 'https://maps.app.goo.gl/dwbBBBkfjkgj8gvA8', + host: 'ACM', + featured: false, + repeats: undefined, + repeatEnds: undefined, + paidEventId: undefined, + }, + }); + + const checkPaidEventId = async (paidEventId: string) => { + try { + const merchEndpoint = getRunEnvironmentConfig().ServiceConfiguration.merch.baseEndpoint; + const ticketEndpoint = getRunEnvironmentConfig().ServiceConfiguration.tickets.baseEndpoint; + const paidEventHref = paidEventId.startsWith('merch:') + ? `${merchEndpoint}/api/v1/merch/details?itemid=${paidEventId.slice(6)}` + : `${ticketEndpoint}/api/v1/event/details?eventid=${paidEventId}`; + const response = await api.get(paidEventHref); + return Boolean(response.status < 299 && response.status >= 200); + } catch (error) { + console.error('Error validating paid event ID:', error); + return false; + } + }; + + const handleSubmit = async (values: EventPostRequest) => { + try { + setIsSubmitting(true); + const realValues = { + ...values, + start: dayjs(values.start).format('YYYY-MM-DD[T]HH:mm:00'), + end: values.end ? dayjs(values.end).format('YYYY-MM-DD[T]HH:mm:00') : undefined, + repeatEnds: + values.repeatEnds && values.repeats + ? dayjs(values.repeatEnds).format('YYYY-MM-DD[T]HH:mm:00') + : undefined, + repeats: values.repeats ? values.repeats : undefined, + }; + + const eventURL = isEditing ? `/api/v1/events/${eventId}` : '/api/v1/events'; + const response = await api.post(eventURL, realValues); + notifications.show({ + title: isEditing ? 'Event updated!' : 'Event created!', + message: isEditing ? undefined : `The event ID is "${response.data.id}".`, + }); + navigate('/events/manage'); + } catch (error) { + setIsSubmitting(false); + console.error('Error creating/editing event:', error); + notifications.show({ + message: 'Failed to create/edit event, please try again.', + }); + } + }; + + return ( +
+ + + + + + + + + + + +

Page Under Construction

+ + +
+ ); +}; From a837785035627314a0dd2808d0b84524bf7a7b36 Mon Sep 17 00:00:00 2001 From: Ethan Chang Date: Sat, 1 Mar 2025 13:23:56 -0600 Subject: [PATCH 02/13] siglead management screen done --- src/ui/pages/siglead/ManageSigLeads.page.tsx | 54 ++------------------ src/ui/pages/siglead/SigScreenComponents.tsx | 47 +++++++++++++++++ 2 files changed, 52 insertions(+), 49 deletions(-) create mode 100644 src/ui/pages/siglead/SigScreenComponents.tsx diff --git a/src/ui/pages/siglead/ManageSigLeads.page.tsx b/src/ui/pages/siglead/ManageSigLeads.page.tsx index 521e8885..0410ad82 100644 --- a/src/ui/pages/siglead/ManageSigLeads.page.tsx +++ b/src/ui/pages/siglead/ManageSigLeads.page.tsx @@ -11,6 +11,7 @@ import { getRunEnvironmentConfig } from '@ui/config'; import { useApi } from '@ui/util/api'; import { OrganizationList as orgList } from '@common/orgs'; import { AppRoles } from '@common/roles'; +import { ScreenComponent } from './SigScreenComponents'; export function capitalizeFirstLetter(string: string) { return string.charAt(0).toUpperCase() + string.slice(1); @@ -154,54 +155,9 @@ export const ManageSigLeadsPage: React.FC = () => { }; return ( -
- - - - - - - - - - - -

Page Under Construction

- - -
+ + SigLead Management System + + ); }; diff --git a/src/ui/pages/siglead/SigScreenComponents.tsx b/src/ui/pages/siglead/SigScreenComponents.tsx new file mode 100644 index 00000000..86290b03 --- /dev/null +++ b/src/ui/pages/siglead/SigScreenComponents.tsx @@ -0,0 +1,47 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { z } from 'zod'; +import { OrganizationList } from '@common/orgs'; +import { NavLink } from '@mantine/core'; +import { AuthGuard } from '@ui/components/AuthGuard'; +import { AppRoles } from '@common/roles'; +import { IconUsersGroup } from '@tabler/icons-react'; +import { useLocation } from 'react-router-dom'; + +// use personn icon +// import { IconPlus, IconTrash } from '@tabler/icons-react'; + +// const OrganizationListEnum = z.enum(OrganizationList); + +// const renderTableRow = (org: string) => { +// const count = 50; +// return( +// +// {(styles) => ( +// +// {org} +// {count} +// +// )} +// +// ) +// } + +const renderSigLink = (org: string, index: number) => { + return ( + + MemberCount[{index}] + + + } + /> + ); +}; + +export const ScreenComponent: React.FC = () => { + return <>{OrganizationList.map(renderSigLink)}; +}; From c75ec3c9c6f5532be8b166a19c72b884290b88a7 Mon Sep 17 00:00:00 2001 From: Ethan Chang Date: Sat, 8 Mar 2025 16:23:37 -0600 Subject: [PATCH 03/13] column headers and text color --- src/ui/pages/siglead/ManageSigLeads.page.tsx | 3 +- src/ui/pages/siglead/SigScreenComponents.tsx | 109 ++++++++++++++++++- 2 files changed, 109 insertions(+), 3 deletions(-) diff --git a/src/ui/pages/siglead/ManageSigLeads.page.tsx b/src/ui/pages/siglead/ManageSigLeads.page.tsx index 0410ad82..741206cb 100644 --- a/src/ui/pages/siglead/ManageSigLeads.page.tsx +++ b/src/ui/pages/siglead/ManageSigLeads.page.tsx @@ -11,7 +11,7 @@ import { getRunEnvironmentConfig } from '@ui/config'; import { useApi } from '@ui/util/api'; import { OrganizationList as orgList } from '@common/orgs'; import { AppRoles } from '@common/roles'; -import { ScreenComponent } from './SigScreenComponents'; +import { ScreenComponent, SigTable } from './SigScreenComponents'; export function capitalizeFirstLetter(string: string) { return string.charAt(0).toUpperCase() + string.slice(1); @@ -158,6 +158,7 @@ export const ManageSigLeadsPage: React.FC = () => { SigLead Management System + {/* */} ); }; diff --git a/src/ui/pages/siglead/SigScreenComponents.tsx b/src/ui/pages/siglead/SigScreenComponents.tsx index 86290b03..2e4e9488 100644 --- a/src/ui/pages/siglead/SigScreenComponents.tsx +++ b/src/ui/pages/siglead/SigScreenComponents.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { z } from 'zod'; import { OrganizationList } from '@common/orgs'; -import { NavLink } from '@mantine/core'; +import { NavLink, Paper } from '@mantine/core'; import { AuthGuard } from '@ui/components/AuthGuard'; import { AppRoles } from '@common/roles'; import { IconUsersGroup } from '@tabler/icons-react'; @@ -31,7 +31,14 @@ const renderSigLink = (org: string, index: number) => { MemberCount[{index}] @@ -43,5 +50,103 @@ const renderSigLink = (org: string, index: number) => { }; export const ScreenComponent: React.FC = () => { - return <>{OrganizationList.map(renderSigLink)}; + return ( + <> + + Organization + Member Count + + {OrganizationList.map(renderSigLink)} + + ); }; + +import { Table } from '@mantine/core'; + +export const SigTable = () => { + const location = useLocation(); + return ( + + {/* Headers */} + + + + + + + + + {OrganizationList.map((org, index) => ( + + {/* Organization Column */} + + + {/* Member Count Column */} + + + ))} + + {/* + {OrganizationList.map((org, index) => ( + + + + ))} + */} +
OrganizationMember Count
+ + + MemberCount[{index}] + +
{renderSigLink(org, index)}
+ ); +}; + +// const navLinks = [ +// { label: "Home", icon: , path: "/" }, +// { label: "Profile", icon: , path: "/profile" }, +// { label: "Settings", icon: , path: "/settings" }, +// ]; + +// export const NavLinkTable = () => { +// return ( +// +// +// +// +// +// +// +// {navLinks.map((link, index) => ( +// +// +// +// ))} +// +//
Navigation
+// +//
+// ); +// } From e89b3017195f00756dc6bca24c06511ab242e3cb Mon Sep 17 00:00:00 2001 From: Ethan Chang Date: Thu, 20 Mar 2025 11:58:44 -0700 Subject: [PATCH 04/13] starter --- src/api/routes/siglead.ts | 466 +++++++++++++++++++ src/ui/pages/siglead/SigScreenComponents.tsx | 6 - 2 files changed, 466 insertions(+), 6 deletions(-) create mode 100644 src/api/routes/siglead.ts diff --git a/src/api/routes/siglead.ts b/src/api/routes/siglead.ts new file mode 100644 index 00000000..a898b086 --- /dev/null +++ b/src/api/routes/siglead.ts @@ -0,0 +1,466 @@ +import { FastifyPluginAsync } from "fastify"; +import { allAppRoles, AppRoles } from "../../common/roles.js"; +import { zodToJsonSchema } from "zod-to-json-schema"; +import { + addToTenant, + getEntraIdToken, + listGroupMembers, + modifyGroup, + patchUserProfile, +} from "../functions/entraId.js"; +import { + BaseError, + DatabaseFetchError, + DatabaseInsertError, + EntraGroupError, + EntraInvitationError, + InternalServerError, + NotFoundError, + UnauthorizedError, +} from "../../common/errors/index.js"; +import { PutItemCommand } from "@aws-sdk/client-dynamodb"; +import { genericConfig } from "../../common/config.js"; +import { marshall } from "@aws-sdk/util-dynamodb"; +import { + InviteUserPostRequest, + invitePostRequestSchema, + GroupMappingCreatePostRequest, + groupMappingCreatePostSchema, + entraActionResponseSchema, + groupModificationPatchSchema, + GroupModificationPatchRequest, + EntraGroupActions, + entraGroupMembershipListResponse, + ProfilePatchRequest, + entraProfilePatchRequest, +} from "../../common/types/iam.js"; +import { + AUTH_DECISION_CACHE_SECONDS, + getGroupRoles, +} from "../functions/authorization.js"; + +const sigleadRoutes: FastifyPluginAsync = async (fastify, _options) => { + fastify.get<{ + Querystring: { groupId: string }; + }>( + "/groups/:groupId/roles", + { + schema: { + querystring: { + type: "object", + properties: { + groupId: { + type: "string", + }, + }, + }, + }, + onRequest: async (request, reply) => { + await fastify.authorize(request, reply, [AppRoles.IAM_ADMIN]); + }, + }, + async (request, reply) => { + try { + const groupId = (request.params as Record).groupId; + const roles = await getGroupRoles( + fastify.dynamoClient, + fastify, + groupId, + ); + return reply.send(roles); + } catch (e: unknown) { + if (e instanceof BaseError) { + throw e; + } + + request.log.error(e); + throw new DatabaseFetchError({ + message: "An error occurred finding the group role mapping.", + }); + } + }, + ); + + // fastify.patch<{ Body: ProfilePatchRequest }>( + // "/profile", + // { + // preValidation: async (request, reply) => { + // await fastify.zodValidateBody(request, reply, entraProfilePatchRequest); + // }, + // onRequest: async (request, reply) => { + // await fastify.authorize(request, reply, allAppRoles); + // }, + // }, + // async (request, reply) => { + // if (!request.tokenPayload || !request.username) { + // throw new UnauthorizedError({ + // message: "User does not have the privileges for this task.", + // }); + // } + // const userOid = request.tokenPayload["oid"]; + // const entraIdToken = await getEntraIdToken( + // { + // smClient: fastify.secretsManagerClient, + // dynamoClient: fastify.dynamoClient, + // }, + // fastify.environmentConfig.AadValidClientId, + // ); + // await patchUserProfile( + // entraIdToken, + // request.username, + // userOid, + // request.body, + // ); + // reply.send(201); + // }, + // ); + // fastify.get<{ + // Body: undefined; + // Querystring: { groupId: string }; + // }>( + // "/groups/:groupId/roles", + // { + // schema: { + // querystring: { + // type: "object", + // properties: { + // groupId: { + // type: "string", + // }, + // }, + // }, + // }, + // onRequest: async (request, reply) => { + // await fastify.authorize(request, reply, [AppRoles.IAM_ADMIN]); + // }, + // }, + // async (request, reply) => { + // try { + // const groupId = (request.params as Record).groupId; + // const roles = await getGroupRoles( + // fastify.dynamoClient, + // fastify, + // groupId, + // ); + // return reply.send(roles); + // } catch (e: unknown) { + // if (e instanceof BaseError) { + // throw e; + // } + + // request.log.error(e); + // throw new DatabaseFetchError({ + // message: "An error occurred finding the group role mapping.", + // }); + // } + // }, + // ); + // fastify.post<{ + // Body: GroupMappingCreatePostRequest; + // Querystring: { groupId: string }; + // }>( + // "/groups/:groupId/roles", + // { + // schema: { + // querystring: { + // type: "object", + // properties: { + // groupId: { + // type: "string", + // }, + // }, + // }, + // }, + // preValidation: async (request, reply) => { + // await fastify.zodValidateBody( + // request, + // reply, + // groupMappingCreatePostSchema, + // ); + // }, + // onRequest: async (request, reply) => { + // await fastify.authorize(request, reply, [AppRoles.IAM_ADMIN]); + // }, + // }, + // async (request, reply) => { + // const groupId = (request.params as Record).groupId; + // try { + // const timestamp = new Date().toISOString(); + // const command = new PutItemCommand({ + // TableName: `${genericConfig.IAMTablePrefix}-grouproles`, + // Item: marshall({ + // groupUuid: groupId, + // roles: request.body.roles, + // createdAt: timestamp, + // }), + // }); + // await fastify.dynamoClient.send(command); + // fastify.nodeCache.set( + // `grouproles-${groupId}`, + // request.body.roles, + // AUTH_DECISION_CACHE_SECONDS, + // ); + // } catch (e: unknown) { + // fastify.nodeCache.del(`grouproles-${groupId}`); + // if (e instanceof BaseError) { + // throw e; + // } + + // request.log.error(e); + // throw new DatabaseInsertError({ + // message: "Could not create group role mapping.", + // }); + // } + // reply.send({ message: "OK" }); + // request.log.info( + // { type: "audit", actor: request.username, target: groupId }, + // `set target roles to ${request.body.roles.toString()}`, + // ); + // }, + // ); + // fastify.post<{ Body: InviteUserPostRequest }>( + // "/inviteUsers", + // { + // schema: { + // response: { 202: zodToJsonSchema(entraActionResponseSchema) }, + // }, + // preValidation: async (request, reply) => { + // await fastify.zodValidateBody(request, reply, invitePostRequestSchema); + // }, + // onRequest: async (request, reply) => { + // await fastify.authorize(request, reply, [AppRoles.IAM_INVITE_ONLY]); + // }, + // }, + // async (request, reply) => { + // const emails = request.body.emails; + // const entraIdToken = await getEntraIdToken( + // { + // smClient: fastify.secretsManagerClient, + // dynamoClient: fastify.dynamoClient, + // }, + // fastify.environmentConfig.AadValidClientId, + // ); + // if (!entraIdToken) { + // throw new InternalServerError({ + // message: "Could not get Entra ID token to perform task.", + // }); + // } + // const response: Record[]> = { + // success: [], + // failure: [], + // }; + // const results = await Promise.allSettled( + // emails.map((email) => addToTenant(entraIdToken, email)), + // ); + // for (let i = 0; i < results.length; i++) { + // const result = results[i]; + // if (result.status === "fulfilled") { + // request.log.info( + // { type: "audit", actor: request.username, target: emails[i] }, + // "invited user to Entra ID tenant.", + // ); + // response.success.push({ email: emails[i] }); + // } else { + // request.log.info( + // { type: "audit", actor: request.username, target: emails[i] }, + // "failed to invite user to Entra ID tenant.", + // ); + // if (result.reason instanceof EntraInvitationError) { + // response.failure.push({ + // email: emails[i], + // message: result.reason.message, + // }); + // } else { + // response.failure.push({ + // email: emails[i], + // message: "An unknown error occurred.", + // }); + // } + // } + // } + // reply.status(202).send(response); + // }, + // ); + // fastify.patch<{ + // Body: GroupModificationPatchRequest; + // Querystring: { groupId: string }; + // }>( + // "/groups/:groupId", + // { + // schema: { + // querystring: { + // type: "object", + // properties: { + // groupId: { + // type: "string", + // }, + // }, + // }, + // }, + // preValidation: async (request, reply) => { + // await fastify.zodValidateBody( + // request, + // reply, + // groupModificationPatchSchema, + // ); + // }, + // onRequest: async (request, reply) => { + // await fastify.authorize(request, reply, [AppRoles.IAM_ADMIN]); + // }, + // }, + // async (request, reply) => { + // const groupId = (request.params as Record).groupId; + // if (!groupId || groupId === "") { + // throw new NotFoundError({ + // endpointName: request.url, + // }); + // } + // if (genericConfig.ProtectedEntraIDGroups.includes(groupId)) { + // throw new EntraGroupError({ + // code: 403, + // message: + // "This group is protected and cannot be modified by this service. You must log into Entra ID directly to modify this group.", + // group: groupId, + // }); + // } + // const entraIdToken = await getEntraIdToken( + // { + // smClient: fastify.secretsManagerClient, + // dynamoClient: fastify.dynamoClient, + // }, + // fastify.environmentConfig.AadValidClientId, + // ); + // const addResults = await Promise.allSettled( + // request.body.add.map((email) => + // modifyGroup(entraIdToken, email, groupId, EntraGroupActions.ADD), + // ), + // ); + // const removeResults = await Promise.allSettled( + // request.body.remove.map((email) => + // modifyGroup(entraIdToken, email, groupId, EntraGroupActions.REMOVE), + // ), + // ); + // const response: Record[]> = { + // success: [], + // failure: [], + // }; + // for (let i = 0; i < addResults.length; i++) { + // const result = addResults[i]; + // if (result.status === "fulfilled") { + // response.success.push({ email: request.body.add[i] }); + // request.log.info( + // { + // type: "audit", + // actor: request.username, + // target: request.body.add[i], + // }, + // `added target to group ID ${groupId}`, + // ); + // } else { + // request.log.info( + // { + // type: "audit", + // actor: request.username, + // target: request.body.add[i], + // }, + // `failed to add target to group ID ${groupId}`, + // ); + // if (result.reason instanceof EntraGroupError) { + // response.failure.push({ + // email: request.body.add[i], + // message: result.reason.message, + // }); + // } else { + // response.failure.push({ + // email: request.body.add[i], + // message: "An unknown error occurred.", + // }); + // } + // } + // } + // for (let i = 0; i < removeResults.length; i++) { + // const result = removeResults[i]; + // if (result.status === "fulfilled") { + // response.success.push({ email: request.body.remove[i] }); + // request.log.info( + // { + // type: "audit", + // actor: request.username, + // target: request.body.remove[i], + // }, + // `removed target from group ID ${groupId}`, + // ); + // } else { + // request.log.info( + // { + // type: "audit", + // actor: request.username, + // target: request.body.add[i], + // }, + // `failed to remove target from group ID ${groupId}`, + // ); + // if (result.reason instanceof EntraGroupError) { + // response.failure.push({ + // email: request.body.add[i], + // message: result.reason.message, + // }); + // } else { + // response.failure.push({ + // email: request.body.add[i], + // message: "An unknown error occurred.", + // }); + // } + // } + // } + // reply.status(202).send(response); + // }, + // ); + // fastify.get<{ + // Querystring: { groupId: string }; + // }>( + // "/groups/:groupId", + // { + // schema: { + // response: { 200: zodToJsonSchema(entraGroupMembershipListResponse) }, + // querystring: { + // type: "object", + // properties: { + // groupId: { + // type: "string", + // }, + // }, + // }, + // }, + // onRequest: async (request, reply) => { + // await fastify.authorize(request, reply, [AppRoles.IAM_ADMIN]); + // }, + // }, + // async (request, reply) => { + // const groupId = (request.params as Record).groupId; + // if (!groupId || groupId === "") { + // throw new NotFoundError({ + // endpointName: request.url, + // }); + // } + // if (genericConfig.ProtectedEntraIDGroups.includes(groupId)) { + // throw new EntraGroupError({ + // code: 403, + // message: + // "This group is protected and cannot be read by this service. You must log into Entra ID directly to read this group.", + // group: groupId, + // }); + // } + // const entraIdToken = await getEntraIdToken( + // { + // smClient: fastify.secretsManagerClient, + // dynamoClient: fastify.dynamoClient, + // }, + // fastify.environmentConfig.AadValidClientId, + // ); + // const response = await listGroupMembers(entraIdToken, groupId); + // reply.status(200).send(response); + // }, + // ); +}; + +export default sigleadRoutes; diff --git a/src/ui/pages/siglead/SigScreenComponents.tsx b/src/ui/pages/siglead/SigScreenComponents.tsx index 2e4e9488..4d102357 100644 --- a/src/ui/pages/siglead/SigScreenComponents.tsx +++ b/src/ui/pages/siglead/SigScreenComponents.tsx @@ -33,12 +33,6 @@ const renderSigLink = (org: string, index: number) => { label={org} variant="filled" active={index % 2 === 0} - // color="blue" - // style={{ - // // color: "lightgray", - // backgroundColor: "DodgerBlue", - // opacity: 0.5 - // }} rightSection={
MemberCount[{index}] From e251c67a3d1887a0a9ff1ce4df44d9dcc1e0cca1 Mon Sep 17 00:00:00 2001 From: Ethan Chang Date: Fri, 28 Mar 2025 15:48:03 -0500 Subject: [PATCH 05/13] stash pop merge conflicts --- src/api/functions/entraId.ts | 2 +- src/api/index.ts | 2 ++ src/api/routes/siglead.ts | 70 +++++++++++++++++------------------- src/common/roles.ts | 21 ++++++----- 4 files changed, 47 insertions(+), 48 deletions(-) diff --git a/src/api/functions/entraId.ts b/src/api/functions/entraId.ts index 44fbe6bf..81a3be97 100644 --- a/src/api/functions/entraId.ts +++ b/src/api/functions/entraId.ts @@ -366,7 +366,7 @@ export async function listGroupMembers( * @throws {EntraUserError} If fetching the user profile fails. * @returns {Promise} The user's profile information. */ -export async function getUserProfile( +export async function getUserProflile( token: string, email: string, ): Promise { diff --git a/src/api/index.ts b/src/api/index.ts index 53678935..774c23be 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -26,6 +26,7 @@ import mobileWalletRoute from "./routes/mobileWallet.js"; import stripeRoutes from "./routes/stripe.js"; import membershipPlugin from "./routes/membership.js"; import path from "path"; // eslint-disable-line import/no-nodejs-modules +import sigleadRoutes from "./routes/siglead.js"; dotenv.config(); @@ -133,6 +134,7 @@ async function init(prettyPrint: boolean = false) { api.register(ticketsPlugin, { prefix: "/tickets" }); api.register(mobileWalletRoute, { prefix: "/mobileWallet" }); api.register(stripeRoutes, { prefix: "/stripe" }); + api.register(sigleadRoutes, { prefix: "/siglead" }); if (app.runEnvironment === "dev") { api.register(vendingPlugin, { prefix: "/vending" }); } diff --git a/src/api/routes/siglead.ts b/src/api/routes/siglead.ts index a898b086..0d2b07be 100644 --- a/src/api/routes/siglead.ts +++ b/src/api/routes/siglead.ts @@ -1,4 +1,4 @@ -import { FastifyPluginAsync } from "fastify"; +import { FastifyInstance, FastifyPluginAsync } from "fastify"; import { allAppRoles, AppRoles } from "../../common/roles.js"; import { zodToJsonSchema } from "zod-to-json-schema"; import { @@ -38,48 +38,42 @@ import { AUTH_DECISION_CACHE_SECONDS, getGroupRoles, } from "../functions/authorization.js"; +import { OrganizationList } from "common/orgs.js"; +import { z } from "zod"; + +const OrganizationListEnum = z.enum(OrganizationList as [string, ...string[]]); +export type Org = z.infer; + +type Member = { name: string; email: string }; +type OrgMembersResponse = { org: Org; members: Member[] }; + +// const groupMappings = getRunEnvironmentConfig().KnownGroupMappings; +// const groupOptions = Object.entries(groupMappings).map(([key, value]) => ({ +// label: userGroupMappings[key as keyof KnownGroups] || key, +// value: `${key}_${value}`, // to ensure that the same group for multiple roles still renders +// })); const sigleadRoutes: FastifyPluginAsync = async (fastify, _options) => { fastify.get<{ - Querystring: { groupId: string }; - }>( - "/groups/:groupId/roles", - { - schema: { - querystring: { - type: "object", - properties: { - groupId: { - type: "string", - }, - }, - }, + Reply: OrgMembersResponse[]; + }>("/groups", async (request, reply) => { + const entraIdToken = await getEntraIdToken( + { + smClient: fastify.secretsManagerClient, + dynamoClient: fastify.dynamoClient, }, - onRequest: async (request, reply) => { - await fastify.authorize(request, reply, [AppRoles.IAM_ADMIN]); - }, - }, - async (request, reply) => { - try { - const groupId = (request.params as Record).groupId; - const roles = await getGroupRoles( - fastify.dynamoClient, - fastify, - groupId, - ); - return reply.send(roles); - } catch (e: unknown) { - if (e instanceof BaseError) { - throw e; - } + fastify.environmentConfig.AadValidClientId, + ); + + const data = await Promise.all( + OrganizationList.map(async (org) => { + const members: Member[] = await listGroupMembers(entraIdToken, org); + return { org, members } as OrgMembersResponse; + }), + ); - request.log.error(e); - throw new DatabaseFetchError({ - message: "An error occurred finding the group role mapping.", - }); - } - }, - ); + reply.status(200).send(data); + }); // fastify.patch<{ Body: ProfilePatchRequest }>( // "/profile", diff --git a/src/common/roles.ts b/src/common/roles.ts index a713b930..9b276b9b 100644 --- a/src/common/roles.ts +++ b/src/common/roles.ts @@ -2,15 +2,18 @@ export const runEnvironments = ["dev", "prod"] as const; export type RunEnvironment = (typeof runEnvironments)[number]; export enum AppRoles { - EVENTS_MANAGER = "manage:events", - SIGLEAD_MANAGER = "manage:siglead", - TICKETS_SCANNER = "scan:tickets", - TICKETS_MANAGER = "manage:tickets", - IAM_ADMIN = "admin:iam", - IAM_INVITE_ONLY = "invite:iam", - STRIPE_LINK_CREATOR = "create:stripeLink", - BYPASS_OBJECT_LEVEL_AUTH = "bypass:ola", + EVENTS_MANAGER = "manage:events", + SIGLEAD_MANAGER = "manage:siglead", + TICKETS_SCANNER = "scan:tickets", + TICKETS_MANAGER = "manage:tickets", + IAM_ADMIN = "admin:iam", + IAM_INVITE_ONLY = "invite:iam", + STRIPE_LINK_CREATOR = "create:stripeLink", + BYPASS_OBJECT_LEVEL_AUTH = "bypass:ola", } export const allAppRoles = Object.values(AppRoles).filter( - (value) => typeof value === "string", + (value) => typeof value === "string", ); + + + \ No newline at end of file From ac54829c02e4c53063362080ab5804ddf2dffc8b Mon Sep 17 00:00:00 2001 From: Ethan Chang Date: Fri, 4 Apr 2025 15:09:49 -0500 Subject: [PATCH 06/13] UI updates for the main screen #99 * changed color of alt tabs to be consistent with the theme * increased size of font of all text * reduced width of table for cleaner look --- src/ui/pages/siglead/ManageSigLeads.page.tsx | 22 +++- src/ui/pages/siglead/SigScreenComponents.tsx | 128 +++---------------- 2 files changed, 37 insertions(+), 113 deletions(-) diff --git a/src/ui/pages/siglead/ManageSigLeads.page.tsx b/src/ui/pages/siglead/ManageSigLeads.page.tsx index 741206cb..4b5fda39 100644 --- a/src/ui/pages/siglead/ManageSigLeads.page.tsx +++ b/src/ui/pages/siglead/ManageSigLeads.page.tsx @@ -1,4 +1,14 @@ -import { Title, Box, TextInput, Textarea, Switch, Select, Button, Loader } from '@mantine/core'; +import { + Title, + Box, + TextInput, + Textarea, + Switch, + Select, + Button, + Loader, + Container, +} from '@mantine/core'; import { DateTimePicker } from '@mantine/dates'; import { useForm, zodResolver } from '@mantine/form'; import { notifications } from '@mantine/notifications'; @@ -11,7 +21,7 @@ import { getRunEnvironmentConfig } from '@ui/config'; import { useApi } from '@ui/util/api'; import { OrganizationList as orgList } from '@common/orgs'; import { AppRoles } from '@common/roles'; -import { ScreenComponent, SigTable } from './SigScreenComponents'; +import { ScreenComponent } from './SigScreenComponents'; export function capitalizeFirstLetter(string: string) { return string.charAt(0).toUpperCase() + string.slice(1); @@ -156,9 +166,11 @@ export const ManageSigLeadsPage: React.FC = () => { return ( - SigLead Management System - - {/* */} + + SigLead Management System + + {/* */} + ); }; diff --git a/src/ui/pages/siglead/SigScreenComponents.tsx b/src/ui/pages/siglead/SigScreenComponents.tsx index 2e4e9488..f34cf995 100644 --- a/src/ui/pages/siglead/SigScreenComponents.tsx +++ b/src/ui/pages/siglead/SigScreenComponents.tsx @@ -1,50 +1,39 @@ import React, { useEffect, useMemo, useState } from 'react'; -import { z } from 'zod'; import { OrganizationList } from '@common/orgs'; import { NavLink, Paper } from '@mantine/core'; -import { AuthGuard } from '@ui/components/AuthGuard'; -import { AppRoles } from '@common/roles'; import { IconUsersGroup } from '@tabler/icons-react'; import { useLocation } from 'react-router-dom'; -// use personn icon -// import { IconPlus, IconTrash } from '@tabler/icons-react'; - -// const OrganizationListEnum = z.enum(OrganizationList); - -// const renderTableRow = (org: string) => { -// const count = 50; -// return( -// -// {(styles) => ( -// -// {org} -// {count} -// -// )} -// -// ) -// } - const renderSigLink = (org: string, index: number) => { + const color = 'light-dark(var(--mantine-color-black), var(--mantine-color-white))'; + const size = '18px'; return ( +
MemberCount[{index}]
} + styles={{ + label: { + color: `${color}`, + fontSize: `${size}`, + }, + }} /> ); }; @@ -60,10 +49,10 @@ export const ScreenComponent: React.FC = () => { justifyContent: 'space-between', alignItems: 'center', fontWeight: 'bold', - // backgroundColor: "#f8f9fa", borderRadius: '8px', padding: '10px 16px', marginBottom: '8px', + fontSize: '22px', }} > Organization @@ -73,80 +62,3 @@ export const ScreenComponent: React.FC = () => { ); }; - -import { Table } from '@mantine/core'; - -export const SigTable = () => { - const location = useLocation(); - return ( - - {/* Headers */} - - - - - - - - - {OrganizationList.map((org, index) => ( - - {/* Organization Column */} - - - {/* Member Count Column */} - - - ))} - - {/* - {OrganizationList.map((org, index) => ( - - - - ))} - */} -
OrganizationMember Count
- - - MemberCount[{index}] - -
{renderSigLink(org, index)}
- ); -}; - -// const navLinks = [ -// { label: "Home", icon: , path: "/" }, -// { label: "Profile", icon: , path: "/profile" }, -// { label: "Settings", icon: , path: "/settings" }, -// ]; - -// export const NavLinkTable = () => { -// return ( -// -// -// -// -// -// -// -// {navLinks.map((link, index) => ( -// -// -// -// ))} -// -//
Navigation
-// -//
-// ); -// } From 11f2d98274a401a8e4a156fdb1b673381ad06cce Mon Sep 17 00:00:00 2001 From: Ethan Chang Date: Sun, 27 Apr 2025 11:34:20 -0500 Subject: [PATCH 07/13] Merged sig-details-api --- .devcontainer/devcontainer.json | 60 +- .github/workflows/deploy-dev.yml | 2 +- .github/workflows/deploy-prod.yml | 6 +- .gitignore | 1 + Makefile | 6 +- README.md | 2 +- cloudformation/iam.yml | 22 + cloudformation/logs.yml | 23 + cloudformation/main.yml | 226 +- package.json | 3 +- src/api/build.js | 21 +- src/api/components/index.ts | 54 + src/api/esbuild.config.js | 1 + src/api/functions/apiKey.ts | 134 ++ src/api/functions/auditLog.ts | 63 + src/api/functions/cloudfrontKvStore.ts | 152 ++ src/api/functions/discord.ts | 2 + src/api/functions/entraId.ts | 59 +- src/api/functions/linkry.ts | 280 +++ src/api/functions/mobileWallet.ts | 15 +- src/api/functions/siglead.ts | 65 + src/api/functions/stripe.ts | 13 + src/api/index.ts | 146 +- src/api/lambda.ts | 1 + src/api/package.json | 9 +- src/api/package.lambda.json | 7 +- src/api/plugins/auth.ts | 63 +- src/api/plugins/authorizeFromSchema.ts | 38 + src/api/plugins/evaluatePolicies.ts | 74 + src/api/plugins/validate.ts | 38 - src/api/public/404.html | 65 + src/api/routes/apiKey.ts | 202 ++ src/api/routes/events.ts | 357 ++- src/api/routes/iam.ts | 327 ++- src/api/routes/ics.ts | 221 +- src/api/routes/linkry.ts | 645 ++++++ src/api/routes/logs.ts | 116 + src/api/routes/membership.ts | 332 +-- src/api/routes/mobileWallet.ts | 47 +- src/api/routes/organizations.ts | 15 +- src/api/routes/protected.ts | 20 +- src/api/routes/roomRequests.ts | 144 +- src/api/routes/siglead.ts | 549 +---- src/api/routes/stripe.ts | 116 +- src/api/routes/tickets.ts | 188 +- src/api/routes/vending.ts | 60 +- src/api/sqs/emailNotifications.ts | 21 +- src/api/sqs/handlers.ts | 15 +- src/api/types.d.ts | 16 +- src/api/zod-openapi-patch.js | 1 + src/common/config.ts | 34 +- src/common/errors/index.ts | 32 + src/common/modules.ts | 28 + src/common/policies/definition.ts | 42 + src/common/policies/evaluator.ts | 71 + src/common/policies/events.ts | 38 + src/common/roles.ts | 6 +- src/common/types/apiKey.ts | 73 + src/common/types/events.ts | 38 + src/common/types/linkry.ts | 47 + src/common/types/logs.ts | 17 + src/common/types/roomRequest.ts | 44 +- src/common/types/siglead.ts | 18 + src/common/types/tickets.ts | 7 + src/ui/Router.tsx | 35 +- src/ui/components/AppShell/index.tsx | 30 +- src/ui/components/AuthContext/index.tsx | 6 +- src/ui/components/BlurredTextDisplay.tsx | 58 + src/ui/main.tsx | 1 + src/ui/package.json | 5 +- src/ui/pages/Error404.page.tsx | 28 +- src/ui/pages/apiKeys/ManageKeys.page.tsx | 34 + src/ui/pages/apiKeys/ManageKeysTable.test.tsx | 213 ++ src/ui/pages/apiKeys/ManageKeysTable.tsx | 449 ++++ src/ui/pages/events/ManageEvent.page.tsx | 176 +- src/ui/pages/linkry/LinkShortener.page.tsx | 368 +++ src/ui/pages/linkry/ManageLink.page.tsx | 265 +++ src/ui/pages/logs/LogRenderer.test.tsx | 290 +++ src/ui/pages/logs/LogRenderer.tsx | 355 +++ src/ui/pages/logs/ViewLogs.page.tsx | 36 + .../roomRequest/RoomRequestLanding.page.tsx | 2 +- src/ui/pages/siglead/ManageSigLeads.page.tsx | 36 + src/ui/pages/siglead/ViewSigLead.page.tsx | 182 ++ src/ui/pages/tickets/SelectEventId.page.tsx | 109 +- src/ui/pages/tickets/ViewTickets.page.tsx | 72 +- src/ui/pages/tos/TermsOfService.page.tsx | 592 +++++ src/ui/types.d.ts | 25 + src/ui/util/api.ts | 5 +- src/ui/vitest.setup.mjs | 1 + tests/live/apiKey.test.ts | 16 + tests/live/documentation.test.ts | 19 + tests/live/events.test.ts | 2 +- tests/live/iam.test.ts | 66 + tests/live/protected.test.ts | 2 +- tests/tsconfig.json | 1 - tests/unit/apiKey.test.ts | 350 +++ tests/unit/auth.test.ts | 6 +- tests/unit/documentation.test.ts | 30 + tests/unit/eventPost.test.ts | 19 +- tests/unit/events.test.ts | 10 +- tests/unit/functions/apiKey.test.ts | 68 + tests/unit/functions/auditLog.test.ts | 158 ++ tests/unit/ical.test.ts | 4 +- tests/unit/linkry.test.ts | 604 +++++ tests/unit/mockLinkryData.testdata.ts | 49 + tests/unit/roomRequests.test.ts | 523 +++++ tests/unit/stripe.test.ts | 23 +- tests/unit/tickets.test.ts | 7 +- tests/unit/vitest.setup.ts | 3 + yarn.lock | 2050 +++++++++++++---- 110 files changed, 11014 insertions(+), 1907 deletions(-) create mode 100644 src/api/components/index.ts create mode 100644 src/api/functions/apiKey.ts create mode 100644 src/api/functions/auditLog.ts create mode 100644 src/api/functions/cloudfrontKvStore.ts create mode 100644 src/api/functions/linkry.ts create mode 100644 src/api/functions/siglead.ts create mode 100644 src/api/plugins/authorizeFromSchema.ts create mode 100644 src/api/plugins/evaluatePolicies.ts delete mode 100644 src/api/plugins/validate.ts create mode 100644 src/api/public/404.html create mode 100644 src/api/routes/apiKey.ts create mode 100644 src/api/routes/linkry.ts create mode 100644 src/api/routes/logs.ts create mode 100644 src/api/zod-openapi-patch.js create mode 100644 src/common/modules.ts create mode 100644 src/common/policies/definition.ts create mode 100644 src/common/policies/evaluator.ts create mode 100644 src/common/policies/events.ts create mode 100644 src/common/types/apiKey.ts create mode 100644 src/common/types/events.ts create mode 100644 src/common/types/linkry.ts create mode 100644 src/common/types/logs.ts create mode 100644 src/common/types/siglead.ts create mode 100644 src/common/types/tickets.ts create mode 100644 src/ui/components/BlurredTextDisplay.tsx create mode 100644 src/ui/pages/apiKeys/ManageKeys.page.tsx create mode 100644 src/ui/pages/apiKeys/ManageKeysTable.test.tsx create mode 100644 src/ui/pages/apiKeys/ManageKeysTable.tsx create mode 100644 src/ui/pages/linkry/LinkShortener.page.tsx create mode 100644 src/ui/pages/linkry/ManageLink.page.tsx create mode 100644 src/ui/pages/logs/LogRenderer.test.tsx create mode 100644 src/ui/pages/logs/LogRenderer.tsx create mode 100644 src/ui/pages/logs/ViewLogs.page.tsx create mode 100644 src/ui/pages/siglead/ViewSigLead.page.tsx create mode 100644 src/ui/pages/tos/TermsOfService.page.tsx create mode 100644 src/ui/types.d.ts create mode 100644 tests/live/apiKey.test.ts create mode 100644 tests/live/documentation.test.ts create mode 100644 tests/live/iam.test.ts create mode 100644 tests/unit/apiKey.test.ts create mode 100644 tests/unit/documentation.test.ts create mode 100644 tests/unit/functions/apiKey.test.ts create mode 100644 tests/unit/functions/auditLog.test.ts create mode 100644 tests/unit/linkry.test.ts create mode 100644 tests/unit/mockLinkryData.testdata.ts create mode 100644 tests/unit/roomRequests.test.ts diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 061cacfc..e369e408 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,41 +1,39 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu { - "name": "Ubuntu", - // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/base:jammy", - "features": { - "ghcr.io/devcontainers/features/node:1": {}, + "name": "Ubuntu", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/base:jammy", + "features": { + "ghcr.io/devcontainers/features/node:1": {}, "ghcr.io/devcontainers/features/aws-cli:1": {}, - "ghcr.io/jungaretti/features/make:1": {}, - "ghcr.io/customink/codespaces-features/sam-cli:1": {}, - "ghcr.io/devcontainers/features/python:1": {} - }, + "ghcr.io/jungaretti/features/make:1": {}, + "ghcr.io/customink/codespaces-features/sam-cli:1": {}, + "ghcr.io/devcontainers/features/python:1": {}, + "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {} + }, - // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, - // Use 'forwardPorts' to make a list of ports inside the container available locally. - "forwardPorts": [ - 8080, - 5173 - ], - "customizations": { - "vscode": { - "extensions": [ - "EditorConfig.EditorConfig", - "waderyan.gitblame", - "Gruntfuggly.todo-tree" - ] - } - } + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [8080, 5173], + "customizations": { + "vscode": { + "extensions": [ + "EditorConfig.EditorConfig", + "waderyan.gitblame", + "Gruntfuggly.todo-tree" + ] + } + } - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "uname -a", + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "uname -a", - // Configure tool-specific properties. - // "customizations": {}, + // Configure tool-specific properties. + // "customizations": {}, - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" } diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index 7d8fe97d..e3f6d824 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -81,7 +81,7 @@ jobs: - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::427040638965:role/GitHubActionsRole - role-session-name: Core_Dev_Deployment + role-session-name: Core_Dev_Deployment_${{ github.run_id }} aws-region: us-east-1 - name: Publish to AWS diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 1f1f09a3..b88c8b84 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -42,6 +42,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: 22.x + - uses: actions/checkout@v4 env: HUSKY: "0" @@ -55,7 +56,7 @@ jobs: - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::427040638965:role/GitHubActionsRole - role-session-name: Core_Dev_Prod_Deployment + role-session-name: Core_Dev_Prod_Deployment_${{ github.run_id }} aws-region: us-east-1 - name: Publish to AWS run: make deploy_dev @@ -90,6 +91,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: 22.x + - uses: actions/checkout@v4 env: HUSKY: "0" @@ -103,7 +105,7 @@ jobs: - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::298118738376:role/GitHubActionsRole - role-session-name: Core_Dev_Prod_Deployment + role-session-name: Core_Dev_Prod_Deployment_${{ github.run_id }} aws-region: us-east-1 - name: Publish to AWS run: make deploy_prod diff --git a/.gitignore b/.gitignore index 4ce03d78..ca5bdd60 100644 --- a/.gitignore +++ b/.gitignore @@ -142,3 +142,4 @@ __pycache__ /blob-report/ /playwright/.cache/ dist_devel/ +!src/ui/pages/logs diff --git a/Makefile b/Makefile index 68e60041..2079edd1 100644 --- a/Makefile +++ b/Makefile @@ -58,7 +58,9 @@ build: src/ cloudformation/ docs/ VITE_BUILD_HASH=$(GIT_HASH) yarn build cp -r src/api/resources/ dist/api/resources rm -rf dist/lambda/sqs - sam build --template-file cloudformation/main.yml + sam build --template-file cloudformation/main.yml --use-container + mkdir -p .aws-sam/build/AppApiLambdaFunction/node_modules/aws-crt/ + cp -r node_modules/aws-crt/dist .aws-sam/build/AppApiLambdaFunction/node_modules/aws-crt local: VITE_BUILD_HASH=$(GIT_HASH) yarn run dev @@ -80,7 +82,7 @@ deploy_dev: check_account_dev build invalidate_cloudfront: @echo "Creating CloudFront invalidation..." $(eval DISTRIBUTION_ID := $(shell aws cloudformation describe-stacks --stack-name $(application_key) --query "Stacks[0].Outputs[?OutputKey=='CloudfrontDistributionId'].OutputValue" --output text)) - $(eval DISTRIBUTION_ID_2 := $(shell aws cloudformation describe-stacks --stack-name $(application_key) --query "Stacks[0].Outputs[?OutputKey=='CloudfrontSecondaryDistributionId'].OutputValue" --output text)) + $(eval DISTRIBUTION_ID_2 := $(shell aws cloudformation describe-stacks --stack-name $(application_key) --query "Stacks[0].Outputs[?OutputKey=='CloudfrontIcalDistributionId'].OutputValue" --output text)) $(eval INVALIDATION_ID := $(shell aws cloudfront create-invalidation --distribution-id $(DISTRIBUTION_ID) --paths "/*" --query 'Invalidation.Id' --output text --no-cli-page)) $(eval INVALIDATION_ID_2 := $(shell aws cloudfront create-invalidation --distribution-id $(DISTRIBUTION_ID_2) --paths "/*" --query 'Invalidation.Id' --output text --no-cli-page)) @echo "Waiting on job $(INVALIDATION_ID)..." diff --git a/README.md b/README.md index 88055351..c9b4255e 100644 --- a/README.md +++ b/README.md @@ -7,4 +7,4 @@ This repository is split into multiple parts: ## Getting Started You will need node>=22 installed, as well as the AWS CLI and the AWS SAM CLI. The best way to work with all of this is to open the environment in a container within your IDE (VS Code should prompt you to do so: use "Clone in Container" for best performance). This container will have all needed software installed. -Then, run `make install` to install all packages, and `make local` to start the UI and API servers! The UI will be accessible on `http://localhost:5173/` and the API on `http://localhost:8080/`. +Then, run `make install` to install all packages, and `make local` to start the UI and API servers! The UI will be accessible on `http://localhost:5173/` and the API on `http://localhost:8080/`. \ No newline at end of file diff --git a/cloudformation/iam.yml b/cloudformation/iam.yml index 7ced4b53..cdc88052 100644 --- a/cloudformation/iam.yml +++ b/cloudformation/iam.yml @@ -15,6 +15,8 @@ Parameters: Type: String SqsQueueArn: Type: String + LinkryKvArn: + Type: String Conditions: IsDev: !Equals [!Ref RunEnvironment, "dev"] @@ -74,12 +76,15 @@ Resources: - Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-membership-external - Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-room-requests - Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-room-requests-status + - Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-linkry + - Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-keys # Index accesses - Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-stripe-links/index/* - Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-events/index/* - Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-merchstore-purchase-history/index/* - Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-room-requests/index/* - Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-room-requests-status/index/* + - Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-linkry/index/* - Sid: DynamoDBCacheAccess Effect: Allow @@ -102,6 +107,16 @@ Resources: Resource: - Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-rate-limiter + - Sid: DynamoDBAuditLogTableAccess + Effect: Allow + Action: + - dynamodb:DescribeTable + - dynamodb:PutItem + - dynamodb:Query + Resource: + - Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-audit-log + - Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-audit-log/index/* + - Sid: DynamoDBStreamAccess Effect: Allow Action: @@ -112,6 +127,12 @@ Resources: Resource: - Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-stripe-links/stream/* - Fn::Sub: arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-events/stream/* + - Sid: CloudfrontKvStreamAccess + Effect: Allow + Action: + - cloudfront-keyvaluestore:* + Resource: + - !Ref LinkryKvArn # API Lambda IAM Role ApiLambdaIAMRole: @@ -176,6 +197,7 @@ Resources: Effect: Allow Resource: - Fn::Sub: arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:infra-core-api-entra* + - Fn::Sub: arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:infra-core-api-ro-entra* # SQS Lambda IAM Role SqsLambdaIAMRole: diff --git a/cloudformation/logs.yml b/cloudformation/logs.yml index ecc01c57..e0db120f 100644 --- a/cloudformation/logs.yml +++ b/cloudformation/logs.yml @@ -21,3 +21,26 @@ Resources: LogGroupName: Fn::Sub: /aws/lambda/${LambdaFunctionName}-edge RetentionInDays: 7 + AppAuditLog: + Type: "AWS::DynamoDB::Table" + DeletionPolicy: "Retain" + UpdateReplacePolicy: "Retain" + Properties: + BillingMode: "PAY_PER_REQUEST" + TableName: infra-core-api-audit-log + DeletionProtectionEnabled: true + PointInTimeRecoverySpecification: + PointInTimeRecoveryEnabled: true + AttributeDefinitions: + - AttributeName: module + AttributeType: S + - AttributeName: createdAt + AttributeType: N + KeySchema: + - AttributeName: module + KeyType: HASH + - AttributeName: createdAt + KeyType: RANGE + TimeToLiveSpecification: + AttributeName: expiresAt + Enabled: true diff --git a/cloudformation/main.yml b/cloudformation/main.yml index 5274b22f..69a15a79 100644 --- a/cloudformation/main.yml +++ b/cloudformation/main.yml @@ -48,7 +48,7 @@ Mappings: LogRetentionDays: 7 SesDomain: "aws.qa.acmuiuc.org" prod: - LogRetentionDays: 365 + LogRetentionDays: 90 SesDomain: "acm.illinois.edu" ApiGwConfig: dev: @@ -87,6 +87,7 @@ Resources: LambdaFunctionName: !Sub ${ApplicationPrefix}-lambda SesEmailDomain: !FindInMap [General, !Ref RunEnvironment, SesDomain] SqsQueueArn: !GetAtt AppSQSQueues.Outputs.MainQueueArn + LinkryKvArn: !GetAtt LinkryRecordsCloudfrontStore.Arn AppLogGroups: Type: AWS::Serverless::Application @@ -125,26 +126,6 @@ Resources: !FindInMap [ApiGwConfig, !Ref RunEnvironment, HostedZoneId] CloudfrontDomain: !GetAtt [AppIcalCloudfrontDistribution, DomainName] - LinkryDomainProxy: - Type: AWS::Serverless::Application - Properties: - Location: ./custom-domain.yml - Parameters: - RunEnvironment: !Ref RunEnvironment - RecordName: go - GWBaseDomainName: !FindInMap - - ApiGwConfig - - !Ref RunEnvironment - - EnvDomainName - GWCertArn: !FindInMap - - ApiGwConfig - - !Ref RunEnvironment - - EnvCertificateArn - GWApiId: !Ref AppApiGateway - GWHostedZoneId: - !FindInMap [ApiGwConfig, !Ref RunEnvironment, HostedZoneId] - CloudfrontDomain: !GetAtt [AppIcalCloudfrontDistribution, DomainName] - CoreUrlProd: Type: AWS::Serverless::Application Properties: @@ -170,7 +151,7 @@ Resources: DependsOn: - AppLogGroups Properties: - Architectures: [arm64] + Architectures: [x86_64] CodeUri: ../dist/lambda AutoPublishAlias: live Runtime: nodejs22.x @@ -184,6 +165,8 @@ Resources: Variables: RunEnvironment: !Ref RunEnvironment EntraRoleArn: !GetAtt AppSecurityRoles.Outputs.EntraFunctionRoleArn + LinkryKvArn: !GetAtt LinkryRecordsCloudfrontStore.Arn + AWS_CRT_NODEJS_BINARY_RELATIVE_PATH: node_modules/aws-crt/dist/bin/linux-x64-glibc/aws-crt-nodejs.node VpcConfig: Ipv6AllowedForDualStack: !If [ShouldAttachVpc, True, !Ref AWS::NoValue] SecurityGroupIds: @@ -215,7 +198,7 @@ Resources: DependsOn: - AppLogGroups Properties: - Architectures: [arm64] + Architectures: [x86_64] CodeUri: ../dist/sqsConsumer AutoPublishAlias: live Runtime: nodejs22.x @@ -290,6 +273,26 @@ Resources: - AttributeName: email KeyType: HASH + ApiKeyTable: + Type: "AWS::DynamoDB::Table" + DeletionPolicy: "Retain" + UpdateReplacePolicy: "Retain" + Properties: + BillingMode: "PAY_PER_REQUEST" + TableName: infra-core-api-keys + DeletionProtectionEnabled: !If [IsProd, true, false] + PointInTimeRecoverySpecification: + PointInTimeRecoveryEnabled: !If [IsProd, true, false] + AttributeDefinitions: + - AttributeName: keyId + AttributeType: S + KeySchema: + - AttributeName: keyId + KeyType: HASH + TimeToLiveSpecification: + AttributeName: expiresAt + Enabled: true + ExternalMembershipRecordsTable: Type: "AWS::DynamoDB::Table" DeletionPolicy: "Retain" @@ -483,6 +486,34 @@ Resources: Projection: ProjectionType: "ALL" + LinkryRecordsTable: + Type: "AWS::DynamoDB::Table" + Properties: + BillingMode: "PAY_PER_REQUEST" + TableName: "infra-core-api-linkry" + DeletionProtectionEnabled: !If [IsProd, true, false] # TODO: remove this + PointInTimeRecoverySpecification: + PointInTimeRecoveryEnabled: !If [IsProd, true, false] + AttributeDefinitions: + - AttributeName: "slug" + AttributeType: "S" + - AttributeName: "access" + AttributeType: "S" + KeySchema: + - AttributeName: "slug" + KeyType: "HASH" + - AttributeName: "access" + KeyType: "RANGE" + GlobalSecondaryIndexes: + - IndexName: "AccessIndex" + KeySchema: + - AttributeName: "access" + KeyType: "HASH" + - AttributeName: "slug" + KeyType: "RANGE" + Projection: + ProjectionType: "ALL" + CacheRecordsTable: Type: "AWS::DynamoDB::Table" DeletionPolicy: "Retain" @@ -725,6 +756,38 @@ Resources: - HEAD CachePolicyId: !Ref CloudfrontCachePolicy OriginRequestPolicyId: 216adef6-5c7f-47e4-b989-5492eafa07d3 + - PathPattern: "/api/v1/organizations" + TargetOriginId: ApiGatewayOrigin + ViewerProtocolPolicy: redirect-to-https + AllowedMethods: + - GET + - HEAD + - OPTIONS + - PUT + - POST + - DELETE + - PATCH + CachedMethods: + - GET + - HEAD + CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6" + OriginRequestPolicyId: 216adef6-5c7f-47e4-b989-5492eafa07d3 + - PathPattern: "/api/documentation*" + TargetOriginId: ApiGatewayOrigin + ViewerProtocolPolicy: redirect-to-https + AllowedMethods: + - GET + - HEAD + - OPTIONS + - PUT + - POST + - DELETE + - PATCH + CachedMethods: + - GET + - HEAD + CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6" + OriginRequestPolicyId: 216adef6-5c7f-47e4-b989-5492eafa07d3 - PathPattern: "/api/*" TargetOriginId: ApiGatewayOrigin ViewerProtocolPolicy: redirect-to-https @@ -831,13 +894,6 @@ Resources: OriginProtocolPolicy: https-only Enabled: true Aliases: - - !Join - - "" - - - "go." - - !FindInMap - - ApiGwConfig - - !Ref RunEnvironment - - EnvDomainName - !Join - "" - - "ical." @@ -875,6 +931,114 @@ Resources: HttpVersion: http2 PriceClass: PriceClass_100 + LinkryRecordsCloudfrontStore: + Type: AWS::CloudFront::KeyValueStore + Properties: + Name: infra-core-api-cloudfront-linkry-kv + + LinkryRecordsCloudfrontFunction: + Type: 'AWS::CloudFront::Function' + Properties: + Name: infra-core-api-cloudfront-linkry-redir + FunctionConfig: + Comment: 'Linkry Redirect Cloudfront Function' + Runtime: 'cloudfront-js-2.0' + KeyValueStoreAssociations: + - KeyValueStoreARN: !Sub '${LinkryRecordsCloudfrontStore.Arn}' + FunctionCode: !Sub | + import cf from 'cloudfront'; + const kvsId = '${LinkryRecordsCloudfrontStore.Id}'; + const kvs = cf.kvs(kvsId); + + async function handler(event) { + const request = event.request; + const path = request.uri.replace(/^\/+/, ''); + if (path === "") { + return { + statusCode: 301, + statusDescription: 'Found', + headers: { + 'location': { value: "https://core.acm.illinois.edu/linkry" } + } + } + } + let redirectUrl = "https://acm.illinois.edu/404"; + try { + const value = await kvs.get(path); + if (value) { + redirectUrl = value; + } + } catch (err) { + console.log('KVS key lookup failed'); + } + var response = { + statusCode: 302, + statusDescription: 'Found', + headers: { + 'location': { value: redirectUrl } + } + }; + return response; + } + AutoPublish: true + + AppLinkryCloudfrontDistribution: + Type: AWS::CloudFront::Distribution + Properties: + DistributionConfig: + Enabled: true + DefaultCacheBehavior: + ViewerProtocolPolicy: redirect-to-https + TargetOriginId: dummyOrigin + ForwardedValues: + QueryString: false + Cookies: + Forward: none + FunctionAssociations: + - EventType: viewer-request + FunctionARN: !GetAtt LinkryRecordsCloudfrontFunction.FunctionARN + Origins: + - Id: dummyOrigin + DomainName: example.com + CustomOriginConfig: + OriginProtocolPolicy: https-only + Aliases: + - !Join + - "" + - - "go." + - !FindInMap + - ApiGwConfig + - !Ref RunEnvironment + - EnvDomainName + ViewerCertificate: + AcmCertificateArn: !FindInMap + - ApiGwConfig + - !Ref RunEnvironment + - EnvCertificateArn + MinimumProtocolVersion: TLSv1.2_2021 + SslSupportMethod: sni-only + HttpVersion: http2 + PriceClass: PriceClass_100 + + LinkryDomainProxy: + Type: AWS::Serverless::Application + Properties: + Location: ./custom-domain.yml + Parameters: + RunEnvironment: !Ref RunEnvironment + RecordName: go + GWBaseDomainName: !FindInMap + - ApiGwConfig + - !Ref RunEnvironment + - EnvDomainName + GWCertArn: !FindInMap + - ApiGwConfig + - !Ref RunEnvironment + - EnvCertificateArn + GWApiId: !Ref AppApiGateway + GWHostedZoneId: + !FindInMap [ApiGwConfig, !Ref RunEnvironment, HostedZoneId] + CloudfrontDomain: !GetAtt [AppLinkryCloudfrontDistribution, DomainName] Outputs: DomainName: @@ -902,7 +1066,7 @@ Outputs: Description: Cloudfront Distribution ID Value: !GetAtt AppFrontendCloudfrontDistribution.Id - CloudfrontSecondaryDistributionId: + CloudfrontIcalDistributionId: Description: Cloudfront Distribution ID Value: !GetAtt AppIcalCloudfrontDistribution.Id diff --git a/package.json b/package.json index c0c0d881..3f065e98 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "lint": "yarn workspaces run lint", "prepare": "node .husky/install.mjs || true", "typecheck": "yarn workspaces run typecheck", - "test:unit": "cross-env RunEnvironment='dev' vitest run --coverage tests/unit --config tests/unit/vitest.config.ts && yarn workspace infra-core-ui run test:unit", + "test:unit": "cross-env RunEnvironment='dev' vitest run --coverage --config tests/unit/vitest.config.ts tests/unit && yarn workspace infra-core-ui run test:unit", "test:unit-ui": "yarn test:unit --ui", "test:unit-watch": "vitest tests/unit", "test:live": "vitest tests/live", @@ -44,6 +44,7 @@ "concurrently": "^9.1.2", "cross-env": "^7.0.3", "esbuild": "^0.23.0", + "esbuild-plugin-copy": "^2.1.1", "eslint": "^8.57.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-typescript": "^18.0.0", diff --git a/src/api/build.js b/src/api/build.js index cb6dff64..20fae8e5 100644 --- a/src/api/build.js +++ b/src/api/build.js @@ -1,5 +1,6 @@ import esbuild from "esbuild"; import { resolve } from "path"; +import { copy } from 'esbuild-plugin-copy' const commonParams = { @@ -15,7 +16,7 @@ const commonParams = { target: "es2022", // Target ES2022 sourcemap: false, platform: "node", - external: ["aws-sdk", "moment-timezone", "passkit-generator", "fastify"], + external: ["aws-sdk", "moment-timezone", "passkit-generator", "fastify", "zod", "zod-openapi", "@fastify/swagger", "@fastify/swagger-ui", "argon2"], alias: { 'moment-timezone': resolve(process.cwd(), '../../node_modules/moment-timezone/builds/moment-timezone-with-data-10-year-range.js') }, @@ -27,8 +28,26 @@ const commonParams = { const require = topLevelCreateRequire(import.meta.url); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); + import "zod-openapi/extend"; `.trim(), }, // Banner for compatibility with CommonJS + plugins: [ + copy({ + resolveFrom: 'cwd', + assets: { + from: ['../../node_modules/@fastify/swagger-ui/static/*'], + to: ['../../dist/lambda/static'], + }, + }), + copy({ + resolveFrom: 'cwd', + assets: { + from: ['./public/*'], + to: ['../../dist/lambda/public'], + }, + }), + ], + inject: [resolve(process.cwd(), "./zod-openapi-patch.js")], } esbuild .build({ diff --git a/src/api/components/index.ts b/src/api/components/index.ts new file mode 100644 index 00000000..fe15c01c --- /dev/null +++ b/src/api/components/index.ts @@ -0,0 +1,54 @@ +import { AppRoles } from "common/roles.js"; +import { FastifyZodOpenApiSchema } from "fastify-zod-openapi"; +import { z } from "zod"; + +export const ts = z.coerce + .number() + .min(0) + .optional() + .openapi({ description: "Staleness bound", example: 0 }); +export const groupId = z.string().min(1).openapi({ + description: "Entra ID Group ID", + example: "d8cbb7c9-2f6d-4b7e-8ba6-b54f8892003b", +}); + +export function withTags( + tags: string[], + schema: T, +) { + return { + tags, + ...schema, + }; +} + +export type RoleSchema = { + "x-required-roles": AppRoles[]; + "x-disable-api-key-auth": boolean; + description: string; +}; + +type RolesConfig = { + disableApiKeyAuth: boolean; +}; + +export function withRoles( + roles: AppRoles[], + schema: T, + { disableApiKeyAuth }: RolesConfig = { disableApiKeyAuth: false }, +): T & RoleSchema { + const security = [{ bearerAuth: [] }] as any; + if (!disableApiKeyAuth) { + security.push({ apiKeyAuth: [] }); + } + return { + security, + "x-required-roles": roles, + "x-disable-api-key-auth": disableApiKeyAuth, + description: + roles.length > 0 + ? `${disableApiKeyAuth ? "API key authentication is not permitted for this route.\n\n" : ""}Requires one of the following roles: ${roles.join(", ")}.${schema.description ? "\n\n" + schema.description : ""}` + : "Requires valid authentication but no specific role.", + ...schema, + }; +} diff --git a/src/api/esbuild.config.js b/src/api/esbuild.config.js index 51098156..847d57fa 100644 --- a/src/api/esbuild.config.js +++ b/src/api/esbuild.config.js @@ -30,6 +30,7 @@ const buildOptions = { const require = topLevelCreateRequire(import.meta.url); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); + import "zod-openapi/extend"; `.trim(), }, // Banner for compatibility with CommonJS plugins: [copyStaticFiles({ diff --git a/src/api/functions/apiKey.ts b/src/api/functions/apiKey.ts new file mode 100644 index 00000000..6979053f --- /dev/null +++ b/src/api/functions/apiKey.ts @@ -0,0 +1,134 @@ +import { createHash, randomBytes } from "crypto"; +import * as argon2 from "argon2"; +import { UnauthenticatedError } from "common/errors/index.js"; +import NodeCache from "node-cache"; +import { + DeleteItemCommand, + DynamoDBClient, + GetItemCommand, +} from "@aws-sdk/client-dynamodb"; +import { genericConfig } from "common/config.js"; +import { AUTH_DECISION_CACHE_SECONDS as API_KEY_DATA_CACHE_SECONDS } from "./authorization.js"; +import { unmarshall } from "@aws-sdk/util-dynamodb"; +import { ApiKeyMaskedEntry, DecomposedApiKey } from "common/types/apiKey.js"; +import { AvailableAuthorizationPolicy } from "common/policies/definition.js"; + +export type ApiKeyDynamoEntry = ApiKeyMaskedEntry & { + keyHash: string; + restrictions?: AvailableAuthorizationPolicy[]; +}; + +function min(a: number, b: number) { + return a < b ? a : b; +} + +export const API_KEY_CACHE_SECONDS = 120; + +export const createChecksum = (key: string) => { + return createHash("sha256").update(key).digest("hex").slice(0, 6); +}; + +export const createApiKey = async () => { + const keyId = randomBytes(6).toString("hex"); + const prefix = `acmuiuc_${keyId}`; + const rawKey = randomBytes(32).toString("hex"); + const checksum = createChecksum(rawKey); + const apiKey = `${prefix}_${rawKey}_${checksum}`; + const hashedKey = await argon2.hash(rawKey); + return { apiKey, hashedKey, keyId }; +}; + +export const getApiKeyParts = (apiKey: string): DecomposedApiKey => { + const [prefix, id, rawKey, checksum] = apiKey.split("_"); + if (!prefix || !id || !rawKey || !checksum) { + throw new UnauthenticatedError({ + message: "Invalid API key.", + }); + } + if ( + prefix != "acmuiuc" || + id.length != 12 || + rawKey.length != 64 || + checksum.length != 6 + ) { + throw new UnauthenticatedError({ + message: "Invalid API key.", + }); + } + return { + prefix, + id, + rawKey, + checksum, + }; +}; + +export const verifyApiKey = async ({ + apiKey, + hashedKey, +}: { + apiKey: string; + hashedKey: string; +}) => { + try { + const { rawKey, checksum: submittedChecksum } = getApiKeyParts(apiKey); + const isChecksumValid = createChecksum(rawKey) === submittedChecksum; + if (!isChecksumValid) { + return false; + } + return await argon2.verify(hashedKey, rawKey); + } catch (e) { + if (e instanceof UnauthenticatedError) { + return false; + } + throw e; + } +}; + +export const getApiKeyData = async ({ + nodeCache, + dynamoClient, + id, +}: { + nodeCache: NodeCache; + dynamoClient: DynamoDBClient; + id: string; +}): Promise => { + const cacheKey = `auth_apikey_${id}`; + const cachedValue = nodeCache.get(`auth_apikey_${id}`); + if (cachedValue !== undefined) { + return cachedValue as ApiKeyDynamoEntry; + } + const getCommand = new GetItemCommand({ + TableName: genericConfig.ApiKeyTable, + Key: { keyId: { S: id } }, + }); + const result = await dynamoClient.send(getCommand); + if (!result || !result.Item) { + nodeCache.set(cacheKey, null, API_KEY_DATA_CACHE_SECONDS); + return undefined; + } + const unmarshalled = unmarshall(result.Item) as ApiKeyDynamoEntry; + if ( + unmarshalled.expiresAt && + unmarshalled.expiresAt <= Math.floor(Date.now() / 1000) + ) { + dynamoClient.send( + new DeleteItemCommand({ + TableName: genericConfig.ApiKeyTable, + Key: { keyId: { S: id } }, + }), + ); // don't need to wait for the response + return undefined; + } + if (!("keyHash" in unmarshalled)) { + return undefined; // bad data, don't cache it + } + let cacheTime = API_KEY_DATA_CACHE_SECONDS; + if (unmarshalled["expiresAt"]) { + const currentEpoch = Date.now(); + cacheTime = min(cacheTime, unmarshalled["expiresAt"] - currentEpoch); + } + nodeCache.set(cacheKey, unmarshalled as ApiKeyDynamoEntry, cacheTime); + return unmarshalled; +}; diff --git a/src/api/functions/auditLog.ts b/src/api/functions/auditLog.ts new file mode 100644 index 00000000..b15f7292 --- /dev/null +++ b/src/api/functions/auditLog.ts @@ -0,0 +1,63 @@ +import { + DynamoDBClient, + PutItemCommand, + type TransactWriteItem, +} from "@aws-sdk/client-dynamodb"; +import { marshall } from "@aws-sdk/util-dynamodb"; +import { genericConfig } from "common/config.js"; +import { AuditLogEntry } from "common/types/logs.js"; + +type AuditLogParams = { + dynamoClient?: DynamoDBClient; + entry: AuditLogEntry; +}; + +const RETENTION_DAYS = 365; + +function buildMarshalledAuditLogItem(entry: AuditLogEntry) { + const baseNow = Date.now(); + const timestamp = Math.floor(baseNow / 1000); + const expireAt = + timestamp + Math.floor((RETENTION_DAYS * 24 * 60 * 60 * 1000) / 1000); + + return marshall({ + ...entry, + createdAt: timestamp, + expireAt, + }); +} + +export async function createAuditLogEntry({ + dynamoClient, + entry, +}: AuditLogParams) { + if (!dynamoClient) { + dynamoClient = new DynamoDBClient({ + region: genericConfig.AwsRegion, + }); + } + + const item = buildMarshalledAuditLogItem(entry); + + const command = new PutItemCommand({ + TableName: genericConfig.AuditLogTable, + Item: item, + }); + + return dynamoClient.send(command); +} + +export function buildAuditLogTransactPut({ + entry, +}: { + entry: AuditLogEntry; +}): TransactWriteItem { + const item = buildMarshalledAuditLogItem(entry); + + return { + Put: { + TableName: genericConfig.AuditLogTable, + Item: item, + }, + }; +} diff --git a/src/api/functions/cloudfrontKvStore.ts b/src/api/functions/cloudfrontKvStore.ts new file mode 100644 index 00000000..3fc09e22 --- /dev/null +++ b/src/api/functions/cloudfrontKvStore.ts @@ -0,0 +1,152 @@ +import { + CloudFrontKeyValueStoreClient, + ConflictException, + DeleteKeyCommand, + DescribeKeyValueStoreCommand, + GetKeyCommand, + PutKeyCommand, +} from "@aws-sdk/client-cloudfront-keyvaluestore"; +import { environmentConfig } from "common/config.js"; +import { + DatabaseDeleteError, + DatabaseFetchError, + DatabaseInsertError, + InternalServerError, +} from "common/errors/index.js"; +import { RunEnvironment } from "common/roles.js"; +import "@aws-sdk/signature-v4-crt"; + +const INITIAL_CONFLICT_WAIT_PERIOD = 150; +const CONFLICT_NUM_RETRIES = 3; + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export const setKey = async ({ + key, + value, + arn, + kvsClient, +}: { + key: string; + value: string; + arn: string; + kvsClient: CloudFrontKeyValueStoreClient; +}) => { + let numRetries = 0; + let currentWaitPeriod = INITIAL_CONFLICT_WAIT_PERIOD; + while (numRetries < CONFLICT_NUM_RETRIES) { + const command = new DescribeKeyValueStoreCommand({ KvsARN: arn }); + const response = await kvsClient.send(command); + const etag = response.ETag; + const putCommand = new PutKeyCommand({ + IfMatch: etag, + Key: key, + Value: value, + KvsARN: arn, + }); + try { + await kvsClient.send(putCommand); + return; + } catch (e) { + if (e instanceof ConflictException) { + numRetries++; + await sleep(currentWaitPeriod); + currentWaitPeriod *= 2; + continue; + } else { + throw e; + } + } + } + throw new DatabaseInsertError({ + message: "Failed to save redirect to Cloudfront KV store.", + }); +}; + +export const deleteKey = async ({ + key, + arn, + kvsClient, +}: { + key: string; + arn: string; + kvsClient: CloudFrontKeyValueStoreClient; +}) => { + let numRetries = 0; + let currentWaitPeriod = INITIAL_CONFLICT_WAIT_PERIOD; + while (numRetries < CONFLICT_NUM_RETRIES) { + const command = new DescribeKeyValueStoreCommand({ KvsARN: arn }); + const response = await kvsClient.send(command); + const etag = response.ETag; + const putCommand = new DeleteKeyCommand({ + IfMatch: etag, + Key: key, + KvsARN: arn, + }); + try { + await kvsClient.send(putCommand); + return; + } catch (e) { + if (e instanceof ConflictException) { + numRetries++; + await sleep(currentWaitPeriod); + currentWaitPeriod *= 2; + continue; + } else { + throw e; + } + } + } + throw new DatabaseDeleteError({ + message: "Failed to save delete to Cloudfront KV store.", + }); +}; + +export const getKey = async ({ + key, + arn, + kvsClient, +}: { + key: string; + arn: string; + kvsClient: CloudFrontKeyValueStoreClient; +}) => { + let numRetries = 0; + let currentWaitPeriod = INITIAL_CONFLICT_WAIT_PERIOD; + while (numRetries < CONFLICT_NUM_RETRIES) { + const getCommand = new GetKeyCommand({ + Key: key, + KvsARN: arn, + }); + try { + const response = await kvsClient.send(getCommand); + return response.Value; + } catch (e) { + if (e instanceof ConflictException) { + numRetries++; + await sleep(currentWaitPeriod); + currentWaitPeriod *= 2; + continue; + } else { + throw e; + } + } + } + throw new DatabaseFetchError({ + message: "Failed to retrieve value from Cloudfront KV store.", + }); +}; + +export const getLinkryKvArn = async (runEnvironment: RunEnvironment) => { + if (process.env.LinkryKvArn) { + return process.env.LinkryKvArn; + } + if (environmentConfig[runEnvironment].LinkryCloudfrontKvArn) { + return environmentConfig[runEnvironment].LinkryCloudfrontKvArn; + } + throw new InternalServerError({ + message: "Could not find the Cloudfront Key-Value store ARN", + }); +}; diff --git a/src/api/functions/discord.ts b/src/api/functions/discord.ts index 5fb92680..a60883be 100644 --- a/src/api/functions/discord.ts +++ b/src/api/functions/discord.ts @@ -27,6 +27,7 @@ const urlRegex = /https:\/\/[a-z0-9\.-]+\/calendar\?id=([a-f0-9-]+)/; export const updateDiscord = async ( smClient: SecretsManagerClient, event: IUpdateDiscord, + actor: string, isDelete: boolean = false, logger: FastifyBaseLogger, ): Promise => { @@ -90,6 +91,7 @@ export const updateDiscord = async ( entityMetadata: { location, }, + reason: `${existingMetadata ? "Modified" : "Created"} by ${actor}.`, }; if (existingMetadata) { diff --git a/src/api/functions/entraId.ts b/src/api/functions/entraId.ts index 81a3be97..bb85f3fa 100644 --- a/src/api/functions/entraId.ts +++ b/src/api/functions/entraId.ts @@ -11,6 +11,7 @@ import { BaseError, EntraFetchError, EntraGroupError, + EntraGroupsFromEmailError, EntraInvitationError, EntraPatchError, InternalServerError, @@ -37,10 +38,13 @@ export async function getEntraIdToken( clients: { smClient: SecretsManagerClient; dynamoClient: DynamoDBClient }, clientId: string, scopes: string[] = ["https://graph.microsoft.com/.default"], + secretName?: string, ) { + if (!secretName) { + secretName = genericConfig.EntraSecretName; + } const secretApiConfig = - (await getSecretValue(clients.smClient, genericConfig.EntraSecretName)) || - {}; + (await getSecretValue(clients.smClient, secretName)) || {}; if ( !secretApiConfig.entra_id_private_key || !secretApiConfig.entra_id_thumbprint @@ -55,7 +59,7 @@ export async function getEntraIdToken( ).toString("utf8"); const cachedToken = await getItemFromCache( clients.dynamoClient, - "entra_id_access_token", + `entra_id_access_token_${secretName}`, ); if (cachedToken) { return cachedToken["token"] as string; @@ -85,7 +89,7 @@ export async function getEntraIdToken( if (result?.accessToken) { await insertItemIntoCache( clients.dynamoClient, - "entra_id_access_token", + `entra_id_access_token_${secretName}`, { token: result?.accessToken }, date, ); @@ -505,3 +509,50 @@ export async function isUserInGroup( }); } } + +export async function listGroupIDsByEmail( + token: string, + email: string, +): Promise> { + try { + const userOid = await resolveEmailToOid(token, email); + const url = `https://graph.microsoft.com/v1.0/users/${userOid}/memberOf`; + const response = await fetch(url, { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = (await response.json()) as { + error?: { message?: string }; + }; + throw new EntraGroupsFromEmailError({ + message: errorData?.error?.message ?? response.statusText, + email, + }); + } + + const data = (await response.json()) as { + value: Array<{ + id: string; + }>; + }; + + // Map the response to the desired format + const groups = data.value.map((group) => group.id); + + return groups; + } catch (error) { + if (error instanceof EntraGroupsFromEmailError) { + throw error; + } + + throw new EntraGroupsFromEmailError({ + message: error instanceof Error ? error.message : String(error), + email, + }); + } +} diff --git a/src/api/functions/linkry.ts b/src/api/functions/linkry.ts new file mode 100644 index 00000000..80c4067d --- /dev/null +++ b/src/api/functions/linkry.ts @@ -0,0 +1,280 @@ +import { + DynamoDBClient, + QueryCommand, + ScanCommand, +} from "@aws-sdk/client-dynamodb"; +import { unmarshall } from "@aws-sdk/util-dynamodb"; +import { LinkryGroupUUIDToGroupNameMap } from "common/config.js"; +import { DelegatedLinkRecord, LinkRecord } from "common/types/linkry.js"; +import { FastifyRequest } from "fastify"; + +export async function fetchLinkEntry( + slug: string, + tableName: string, + dynamoClient: DynamoDBClient, +): Promise { + const fetchLinkEntry = new QueryCommand({ + TableName: tableName, + KeyConditionExpression: "slug = :slug", + ExpressionAttributeValues: { + ":slug": { S: slug }, + }, + ScanIndexForward: false, + }); + const result = await dynamoClient.send(fetchLinkEntry); + if (!result.Items || result.Items.length == 0) { + return null; + } + const unmarshalled = result.Items.map((x) => unmarshall(x)); + const ownerRecord = unmarshalled.filter((x) => + (x["access"] as string).startsWith("OWNER#"), + )[0]; + return { + ...ownerRecord, + access: unmarshalled + .filter((x) => (x["access"] as string).startsWith("GROUP#")) + .map((x) => (x["access"] as string).replace("GROUP#", "")), + owner: ownerRecord["access"].replace("OWNER#", ""), + } as LinkRecord; +} + +export async function fetchOwnerRecords( + username: string, + tableName: string, + dynamoClient: DynamoDBClient, +) { + const fetchAllOwnerRecords = new QueryCommand({ + TableName: tableName, + IndexName: "AccessIndex", + KeyConditionExpression: "#access = :accessVal", + ExpressionAttributeNames: { + "#access": "access", + }, + ExpressionAttributeValues: { + ":accessVal": { S: `OWNER#${username}` }, + }, + ScanIndexForward: false, + }); + + const result = await dynamoClient.send(fetchAllOwnerRecords); + + // Process the results + return (result.Items || []).map((item) => { + const unmarshalledItem = unmarshall(item); + + // Strip '#' from access field + if (unmarshalledItem.access) { + unmarshalledItem.access = + unmarshalledItem.access.split("#")[1] || unmarshalledItem.access; + } + + return unmarshalledItem as LinkRecord; + }); +} + +export function extractUniqueSlugs(records: LinkRecord[]) { + return Array.from( + new Set(records.filter((item) => item.slug).map((item) => item.slug)), + ); +} + +export async function getGroupsForSlugs( + slugs: string[], + ownerRecords: LinkRecord[], + tableName: string, + dynamoClient: DynamoDBClient, +) { + const groupsPromises = slugs.map(async (slug) => { + const groupQueryCommand = new QueryCommand({ + TableName: tableName, + KeyConditionExpression: + "#slug = :slugVal AND begins_with(#access, :accessVal)", + ExpressionAttributeNames: { + "#slug": "slug", + "#access": "access", + }, + ExpressionAttributeValues: { + ":slugVal": { S: slug }, + ":accessVal": { S: "GROUP#" }, + }, + ScanIndexForward: false, + }); + + try { + const response = await dynamoClient.send(groupQueryCommand); + const groupItems = (response.Items || []).map((item) => unmarshall(item)); + const groupIds = groupItems.map((item) => + item.access.replace("GROUP#", ""), + ); + const originalRecord = + ownerRecords.find((item) => item.slug === slug) || {}; + + return { + ...originalRecord, + access: groupIds, + }; + } catch (error) { + console.error(`Error fetching groups for slug ${slug}:`, error); + const originalRecord = + ownerRecords.find((item) => item.slug === slug) || {}; + return { + ...originalRecord, + access: [], + }; + } + }); + + const results = await Promise.allSettled(groupsPromises); + + return results + .filter((result) => result.status === "fulfilled") + .map((result) => result.value); +} + +export function getFilteredUserGroups(request: FastifyRequest) { + const userGroupMembershipIds = request.tokenPayload?.groups || []; + return userGroupMembershipIds.filter((groupId) => + [...LinkryGroupUUIDToGroupNameMap.keys()].includes(groupId), + ); +} + +export async function getAllLinks( + tableName: string, + dynamoClient: DynamoDBClient, +): Promise { + const scan = new ScanCommand({ + TableName: tableName, + }); + const response = await dynamoClient.send(scan); + const unmarshalled = (response.Items || []).map((item) => unmarshall(item)); + const ownerRecords = unmarshalled.filter((x) => + (x["access"] as string).startsWith("OWNER#"), + ); + const delegations = unmarshalled.filter( + (x) => !(x["access"] as string).startsWith("OWNER#"), + ); + const accessGroupMap: Record = {}; // maps slug to access groups + for (const deleg of delegations) { + if (deleg.slug in accessGroupMap) { + accessGroupMap[deleg.slug].push(deleg.access.replace("GROUP#", "")); + } else { + accessGroupMap[deleg.slug] = [deleg.access.replace("GROUP#", "")]; + } + } + return ownerRecords.map((x) => ({ + ...x, + access: accessGroupMap[x.slug], + owner: x["access"].replace("OWNER#", ""), + })) as LinkRecord[]; +} + +export async function getDelegatedLinks( + userGroups: string[], + ownedSlugs: string[], + tableName: string, + dynamoClient: DynamoDBClient, +): Promise { + const groupQueries = userGroups.map(async (groupId) => { + try { + const groupQueryCommand = new QueryCommand({ + TableName: tableName, + IndexName: "AccessIndex", + KeyConditionExpression: "#access = :accessVal", + ExpressionAttributeNames: { + "#access": "access", + }, + ExpressionAttributeValues: { + ":accessVal": { S: `GROUP#${groupId}` }, + }, + }); + + const response = await dynamoClient.send(groupQueryCommand); + const items = (response.Items || []).map((item) => unmarshall(item)); + + // Get unique only + const delegatedSlugs = [ + ...new Set( + items + .filter((item) => item.slug && !ownedSlugs.includes(item.slug)) + .map((item) => item.slug), + ), + ]; + + if (!delegatedSlugs.length) return []; + + // Fetch entry records + const results = await Promise.all( + delegatedSlugs.map(async (slug) => { + try { + const ownerQuery = new QueryCommand({ + TableName: tableName, + KeyConditionExpression: + "#slug = :slugVal AND begins_with(#access, :ownerVal)", + ExpressionAttributeNames: { + "#slug": "slug", + "#access": "access", + }, + ExpressionAttributeValues: { + ":slugVal": { S: slug }, + ":ownerVal": { S: "OWNER#" }, + }, + }); + + const ownerResponse = await dynamoClient.send(ownerQuery); + const ownerRecord = ownerResponse.Items + ? unmarshall(ownerResponse.Items[0]) + : null; + + if (!ownerRecord) return null; + const groupQuery = new QueryCommand({ + TableName: tableName, + KeyConditionExpression: + "#slug = :slugVal AND begins_with(#access, :groupVal)", + ExpressionAttributeNames: { + "#slug": "slug", + "#access": "access", + }, + ExpressionAttributeValues: { + ":slugVal": { S: slug }, + ":groupVal": { S: "GROUP#" }, + }, + }); + + const groupResponse = await dynamoClient.send(groupQuery); + const groupItems = (groupResponse.Items || []).map((item) => + unmarshall(item), + ); + const groupIds = groupItems.map((item) => + item.access.replace("GROUP#", ""), + ); + return { + ...ownerRecord, + access: groupIds, + owner: ownerRecord.access.replace("OWNER#", ""), + } as DelegatedLinkRecord; + } catch (error) { + console.error(`Error processing delegated slug ${slug}:`, error); + return null; + } + }), + ); + + return results.filter(Boolean); + } catch (error) { + console.error(`Error processing group ${groupId}:`, error); + return []; + } + }); + const results = await Promise.allSettled(groupQueries); + const allDelegatedLinks = results + .filter((result) => result.status === "fulfilled") + .flatMap((result) => result.value); + const slugMap = new Map(); + allDelegatedLinks.forEach((link) => { + if (link && link.slug && !slugMap.has(link.slug)) { + slugMap.set(link.slug, link); + } + }); + + return Array.from(slugMap.values()); +} diff --git a/src/api/functions/mobileWallet.ts b/src/api/functions/mobileWallet.ts index c2c0986d..915c5a93 100644 --- a/src/api/functions/mobileWallet.ts +++ b/src/api/functions/mobileWallet.ts @@ -17,6 +17,8 @@ import { promises as fs } from "fs"; import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; import { RunEnvironment } from "common/roles.js"; import pino from "pino"; +import { createAuditLogEntry } from "./auditLog.js"; +import { Modules } from "common/modules.js"; function trim(s: string) { return (s || "").replace(/^\s+|\s+$/g, ""); @@ -66,7 +68,6 @@ export async function issueAppleWalletMembershipCard( "base64", ).toString("utf-8"); pass["passTypeIdentifier"] = environmentConfig["PasskitIdentifier"]; - const pkpass = new PKPass( { "icon.png": await fs.readFile(icon), @@ -117,9 +118,13 @@ export async function issueAppleWalletMembershipCard( pkpass.backFields.push({ label: "Pass Created On", key: "iat", value: iat }); pkpass.backFields.push({ label: "Membership ID", key: "id", value: email }); const buffer = pkpass.getAsBuffer(); - logger.info( - { type: "audit", module: "mobileWallet", actor: initiator, target: email }, - "Created membership verification pass", - ); + await createAuditLogEntry({ + entry: { + module: Modules.MOBILE_WALLET, + actor: initiator, + target: email, + message: "Created membership verification pass", + }, + }); return buffer; } diff --git a/src/api/functions/siglead.ts b/src/api/functions/siglead.ts new file mode 100644 index 00000000..763c21af --- /dev/null +++ b/src/api/functions/siglead.ts @@ -0,0 +1,65 @@ +import { + DynamoDBClient, + QueryCommand, + ScanCommand, +} from "@aws-sdk/client-dynamodb"; +import { unmarshall } from "@aws-sdk/util-dynamodb"; +import { SigDetailRecord, SigMemberRecord } from "common/types/siglead.js"; +import { FastifyRequest } from "fastify"; + +export async function fetchMemberRecords( + sigid: string, + tableName: string, + dynamoClient: DynamoDBClient, +) { + const fetchSigMemberRecords = new QueryCommand({ + TableName: tableName, + KeyConditionExpression: "#sigid = :accessVal", + ExpressionAttributeNames: { + "#sigid": "sigGroupId", + }, + ExpressionAttributeValues: { + ":accessVal": { S: sigid }, + }, + ScanIndexForward: false, + }); + + const result = await dynamoClient.send(fetchSigMemberRecords); + + // Process the results + return (result.Items || []).map((item) => { + const unmarshalledItem = unmarshall(item); + return unmarshalledItem as SigMemberRecord; + }); +} + +export async function fetchSigDetail( + sigid: string, + tableName: string, + dynamoClient: DynamoDBClient, +) { + const fetchSigDetail = new QueryCommand({ + TableName: tableName, + KeyConditionExpression: "#sigid = :accessVal", + ExpressionAttributeNames: { + "#sigid": "sigid", + }, + ExpressionAttributeValues: { + ":accessVal": { S: sigid }, + }, + ScanIndexForward: false, + }); + + const result = await dynamoClient.send(fetchSigDetail); + + // Process the results + return (result.Items || [{}]).map((item) => { + const unmarshalledItem = unmarshall(item); + + // Strip '#' from access field + delete unmarshalledItem.leadGroupId; + delete unmarshalledItem.memberGroupId; + + return unmarshalledItem as SigDetailRecord; + })[0]; +} diff --git a/src/api/functions/stripe.ts b/src/api/functions/stripe.ts index 0faa97e3..31f7f275 100644 --- a/src/api/functions/stripe.ts +++ b/src/api/functions/stripe.ts @@ -98,3 +98,16 @@ export const createCheckoutSession = async ({ } return session.url; }; + +export const deactivateStripeLink = async ({ + linkId, + stripeApiKey, +}: { + linkId: string; + stripeApiKey: string; +}): Promise => { + const stripe = new Stripe(stripeApiKey); + await stripe.paymentLinks.update(linkId, { + active: false, + }); +}; diff --git a/src/api/index.ts b/src/api/index.ts index 5ae881ce..7a810d5c 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,4 +1,5 @@ /* eslint import/no-nodejs-modules: ["error", {"allow": ["crypto"]}] */ +import "zod-openapi/extend"; import { randomUUID } from "crypto"; import fastify, { FastifyInstance } from "fastify"; import FastifyAuthProvider from "@fastify/auth"; @@ -10,14 +11,17 @@ import { RunEnvironment, runEnvironments } from "../common/roles.js"; import { InternalServerError } from "../common/errors/index.js"; import eventsPlugin from "./routes/events.js"; import cors from "@fastify/cors"; -import fastifyZodValidationPlugin from "./plugins/validate.js"; import { environmentConfig, genericConfig } from "../common/config.js"; import organizationsPlugin from "./routes/organizations.js"; +import authorizeFromSchemaPlugin from "./plugins/authorizeFromSchema.js"; +import evaluatePoliciesPlugin from "./plugins/evaluatePolicies.js"; import icalPlugin from "./routes/ics.js"; import vendingPlugin from "./routes/vending.js"; import * as dotenv from "dotenv"; import iamRoutes from "./routes/iam.js"; import ticketsPlugin from "./routes/tickets.js"; +import linkryRoutes from "./routes/linkry.js"; +import sigleadRoutes from "./routes/siglead.js"; import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts"; import NodeCache from "node-cache"; import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; @@ -26,8 +30,20 @@ import mobileWalletRoute from "./routes/mobileWallet.js"; import stripeRoutes from "./routes/stripe.js"; import membershipPlugin from "./routes/membership.js"; import path from "path"; // eslint-disable-line import/no-nodejs-modules -import sigleadRoutes from "./routes/siglead.js"; import roomRequestRoutes from "./routes/roomRequests.js"; +import logsPlugin from "./routes/logs.js"; +import fastifySwagger from "@fastify/swagger"; +import fastifySwaggerUI from "@fastify/swagger-ui"; +import { + fastifyZodOpenApiPlugin, + fastifyZodOpenApiTransform, + fastifyZodOpenApiTransformObject, + serializerCompiler, + validatorCompiler, +} from "fastify-zod-openapi"; +import { ZodOpenApiVersion } from "zod-openapi"; +import { withTags } from "./components/index.js"; +import apiKeyRoute from "./routes/apiKey.js"; dotenv.config(); @@ -63,8 +79,6 @@ async function init(prettyPrint: boolean = false) { const customDomainBaseMappers: Record = { "ical.acm.illinois.edu": `/api/v1/ical${url}`, "ical.aws.qa.acmuiuc.org": `/api/v1/ical${url}`, - "go.acm.illinois.edu": `/api/v1/linkry/redir${url}`, - "go.aws.qa.acmuiuc.org": `/api/v1/linkry/redir${url}`, }; if (hostname in customDomainBaseMappers) { return customDomainBaseMappers[hostname]; @@ -82,10 +96,117 @@ async function init(prettyPrint: boolean = false) { return event.requestContext.requestId; }, }); + app.setValidatorCompiler(validatorCompiler); + app.setSerializerCompiler(serializerCompiler); + await app.register(authorizeFromSchemaPlugin); await app.register(fastifyAuthPlugin); - await app.register(fastifyZodValidationPlugin); await app.register(FastifyAuthProvider); + await app.register(evaluatePoliciesPlugin); await app.register(errorHandlerPlugin); + await app.register(fastifyZodOpenApiPlugin); + await app.register(fastifySwagger, { + openapi: { + info: { + title: "ACM @ UIUC Core API", + description: "ACM @ UIUC Core Management Platform", + version: "1.0.0", + contact: { + name: "ACM @ UIUC Infrastructure Team", + email: "infra@acm.illinois.edu", + url: "infra.acm.illinois.edu", + }, + license: { + name: "BSD 3-Clause", + identifier: "BSD-3-Clause", + url: "https://github.com/acm-uiuc/core/blob/main/LICENSE", + }, + termsOfService: "https://core.acm.illinois.edu/tos", + }, + servers: [ + { + url: "https://core.acm.illinois.edu/", + description: "Production API server", + }, + { + url: "https://core.aws.qa.acmuiuc.org/", + description: "QA API server", + }, + ], + tags: [ + { + name: "Events", + description: + "Retrieve ACM @ UIUC-wide and organization-specific calendars and event metadata.", + }, + { + name: "Generic", + description: "Retrieve metadata about a user or ACM @ UIUC .", + }, + { + name: "iCalendar Integration", + description: + "Retrieve Events calendars in iCalendar format (for integration with external calendar clients).", + }, + { + name: "IAM", + description: "Identity and Access Management for internal services.", + }, + { name: "Linkry", description: "Link Shortener." }, + { + name: "Logging", + description: "View audit logs for various services.", + }, + { + name: "Membership", + description: "Purchasing or checking ACM @ UIUC membership.", + }, + { + name: "Tickets/Merchandise", + description: "Handling the tickets and merchandise lifecycle.", + }, + { + name: "Mobile Wallet", + description: "Issuing Apple/Google Wallet passes.", + }, + { + name: "Stripe", + description: + "Collecting payments for ACM @ UIUC invoices and other services.", + }, + { + name: "Room Requests", + description: + "Creating room reservation requests for ACM @ UIUC within University buildings.", + }, + { + name: "API Keys", + description: "Manage the lifecycle of API keys.", + }, + ], + openapi: "3.1.0" satisfies ZodOpenApiVersion, // If this is not specified, it will default to 3.1.0 + components: { + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + description: + "Authorization: Bearer {token}\n\nThis API uses JWT tokens issued by Entra ID (Azure AD) with the Core API audience. Tokens must be included in the Authorization header as a Bearer token for all protected endpoints.", + }, + apiKeyAuth: { + type: "apiKey", + in: "header", + name: "X-Api-Key", + }, + }, + }, + }, + transform: fastifyZodOpenApiTransform, + transformObject: fastifyZodOpenApiTransformObject, + }); + await app.register(fastifySwaggerUI, { + routePrefix: "/api/documentation", + }); await app.register(fastifyStatic, { root: path.join(__dirname, "public"), prefix: "/", @@ -123,7 +244,15 @@ async function init(prettyPrint: boolean = false) { ); done(); }); - app.get("/api/v1/healthz", (_, reply) => reply.send({ message: "UP" })); + app.get( + "/api/v1/healthz", + { + schema: withTags(["Generic"], { + summary: "Verify that the API server is healthy.", + }), + }, + (_, reply) => reply.send({ message: "UP" }), + ); await app.register( async (api, _options) => { api.register(protectedRoute, { prefix: "/protected" }); @@ -133,10 +262,13 @@ async function init(prettyPrint: boolean = false) { api.register(icalPlugin, { prefix: "/ical" }); api.register(iamRoutes, { prefix: "/iam" }); api.register(ticketsPlugin, { prefix: "/tickets" }); + api.register(linkryRoutes, { prefix: "/linkry" }); + api.register(sigleadRoutes, { prefix: "/siglead" }); api.register(mobileWalletRoute, { prefix: "/mobileWallet" }); api.register(stripeRoutes, { prefix: "/stripe" }); - api.register(sigleadRoutes, { prefix: "/siglead" }); api.register(roomRequestRoutes, { prefix: "/roomRequests" }); + api.register(logsPlugin, { prefix: "/logs" }); + api.register(apiKeyRoute, { prefix: "/apiKey" }); if (app.runEnvironment === "dev") { api.register(vendingPlugin, { prefix: "/vending" }); } diff --git a/src/api/lambda.ts b/src/api/lambda.ts index 0cd9a769..e1a91500 100644 --- a/src/api/lambda.ts +++ b/src/api/lambda.ts @@ -1,5 +1,6 @@ /* eslint-disable */ +import "zod-openapi/extend"; import awsLambdaFastify from "@fastify/aws-lambda"; import init from "./index.js"; diff --git a/src/api/package.json b/src/api/package.json index f7eb3201..46842ccb 100644 --- a/src/api/package.json +++ b/src/api/package.json @@ -15,11 +15,13 @@ "prettier:write": "prettier --write *.ts **/*.ts" }, "dependencies": { + "@aws-sdk/client-cloudfront-keyvaluestore": "^3.787.0", "@aws-sdk/client-dynamodb": "^3.624.0", "@aws-sdk/client-secrets-manager": "^3.624.0", "@aws-sdk/client-ses": "^3.734.0", "@aws-sdk/client-sqs": "^3.738.0", "@aws-sdk/client-sts": "^3.758.0", + "@aws-sdk/signature-v4-crt": "^3.787.0", "@aws-sdk/util-dynamodb": "^3.624.0", "@azure/msal-node": "^2.16.1", "@fastify/auth": "^5.0.1", @@ -27,17 +29,21 @@ "@fastify/caching": "^9.0.1", "@fastify/cors": "^10.0.1", "@fastify/static": "^8.1.1", + "@fastify/swagger": "^9.5.0", + "@fastify/swagger-ui": "^5.2.2", "@middy/core": "^6.0.0", "@middy/event-normalizer": "^6.0.0", "@middy/sqs-partial-batch-failure": "^6.0.0", "@touch4it/ical-timezones": "^1.9.0", + "argon2": "^0.41.1", "base64-arraybuffer": "^1.0.2", "discord.js": "^14.15.3", "dotenv": "^16.4.5", "esbuild": "^0.24.2", - "fastify": "^5.1.0", + "fastify": "^5.3.2", "fastify-plugin": "^4.5.1", "fastify-raw-body": "^5.0.0", + "fastify-zod-openapi": "^4.1.1", "ical-generator": "^7.2.0", "jsonwebtoken": "^9.0.2", "jwks-rsa": "^3.1.0", @@ -51,6 +57,7 @@ "stripe": "^17.6.0", "uuid": "^11.0.5", "zod": "^3.23.8", + "zod-openapi": "^4.2.4", "zod-to-json-schema": "^3.23.2", "zod-validation-error": "^3.3.1" }, diff --git a/src/api/package.lambda.json b/src/api/package.lambda.json index ae609ba8..69de8087 100644 --- a/src/api/package.lambda.json +++ b/src/api/package.lambda.json @@ -9,7 +9,12 @@ "dependencies": { "moment-timezone": "^0.5.45", "passkit-generator": "^3.3.1", - "fastify": "^5.1.0" + "fastify": "^5.3.2", + "@fastify/swagger": "^9.5.0", + "@fastify/swagger-ui": "^5.2.2", + "zod": "^3.23.8", + "zod-openapi": "^4.2.4", + "argon2": "^0.41.1" }, "devDependencies": {} } diff --git a/src/api/plugins/auth.ts b/src/api/plugins/auth.ts index 4453c27f..f7e75604 100644 --- a/src/api/plugins/auth.ts +++ b/src/api/plugins/auth.ts @@ -6,7 +6,7 @@ import { SecretsManagerClient, GetSecretValueCommand, } from "@aws-sdk/client-secrets-manager"; -import { AppRoles } from "../../common/roles.js"; +import { AppRoles } from "common/roles.js"; import { BaseError, InternalServerError, @@ -15,8 +15,9 @@ import { } from "../../common/errors/index.js"; import { genericConfig, SecretConfig } from "../../common/config.js"; import { getGroupRoles, getUserRoles } from "../functions/authorization.js"; +import { getApiKeyData, getApiKeyParts } from "api/functions/apiKey.js"; -function intersection(setA: Set, setB: Set): Set { +export function intersection(setA: Set, setB: Set): Set { const _intersection = new Set(); for (const elem of setB) { if (setA.has(elem)) { @@ -75,16 +76,70 @@ export const getSecretValue = async ( }; const authPlugin: FastifyPluginAsync = async (fastify, _options) => { + const handleApiKeyAuthentication = async ( + request: FastifyRequest, + _reply: FastifyReply, + validRoles: AppRoles[], + ): Promise> => { + const apiKeyValueTemp = request.headers["x-api-key"]; + if (!apiKeyValueTemp) { + throw new UnauthenticatedError({ + message: "Invalid API key.", + }); + } + const apiKeyValue = + typeof apiKeyValueTemp === "string" + ? apiKeyValueTemp + : apiKeyValueTemp[0]; + const { id: apikeyId } = getApiKeyParts(apiKeyValue); + const keyData = await getApiKeyData({ + nodeCache: fastify.nodeCache, + dynamoClient: fastify.dynamoClient, + id: apikeyId, + }); + if (!keyData) { + throw new UnauthenticatedError({ + message: "Invalid API key.", + }); + } + const expectedRoles = new Set(validRoles); + const rolesSet = new Set(keyData.roles); + if ( + expectedRoles.size > 0 && + intersection(rolesSet, expectedRoles).size === 0 + ) { + throw new UnauthorizedError({ + message: "User does not have the privileges for this task.", + }); + } + request.username = `acmuiuc_${apikeyId}`; + request.userRoles = rolesSet; + request.tokenPayload = undefined; // there's no token data + request.policyRestrictions = keyData.restrictions; + return new Set(keyData.roles); + }; fastify.decorate( "authorize", async function ( request: FastifyRequest, - _reply: FastifyReply, + reply: FastifyReply, validRoles: AppRoles[], + disableApiKeyAuth: boolean, ): Promise> { const userRoles = new Set([] as AppRoles[]); try { - const authHeader = request.headers.authorization; + if (!disableApiKeyAuth) { + const apiKeyHeader = request.headers + ? request.headers["x-api-key"] + : null; + if (apiKeyHeader) { + return handleApiKeyAuthentication(request, reply, validRoles); + } + } + + const authHeader = request.headers + ? request.headers["authorization"] + : null; if (!authHeader) { throw new UnauthenticatedError({ message: "Did not find bearer token in expected header.", diff --git a/src/api/plugins/authorizeFromSchema.ts b/src/api/plugins/authorizeFromSchema.ts new file mode 100644 index 00000000..7c6c6b60 --- /dev/null +++ b/src/api/plugins/authorizeFromSchema.ts @@ -0,0 +1,38 @@ +import { FastifyPluginAsync } from "fastify"; +import { InternalServerError } from "common/errors/index.js"; +import fp from "fastify-plugin"; +import { FastifyZodOpenApiSchema } from "fastify-zod-openapi"; +import { RoleSchema } from "api/components/index.js"; + +declare module "fastify" { + interface FastifyInstance { + authorizeFromSchema: ( + request: FastifyRequest, + reply: FastifyReply, + ) => Promise; + } +} + +const authorizeFromSchemaPlugin: FastifyPluginAsync = fp(async (fastify) => { + fastify.decorate("authorizeFromSchema", async (request, reply) => { + const schema = request.routeOptions?.schema; + if (!schema || !("x-required-roles" in schema)) { + throw new InternalServerError({ + message: + "Server has not specified authentication roles for this route.", + }); + } + if (!schema || !("x-disable-api-key-auth" in schema)) { + throw new InternalServerError({ + message: + "Server has not specified available authentication methods for this route.", + }); + } + const realSchema = schema as FastifyZodOpenApiSchema & RoleSchema; + const roles = realSchema["x-required-roles"]; + const disableApiKeyAuth = realSchema["x-disable-api-key-auth"]; + await fastify.authorize(request, reply, roles, disableApiKeyAuth); + }); +}); + +export default authorizeFromSchemaPlugin; diff --git a/src/api/plugins/evaluatePolicies.ts b/src/api/plugins/evaluatePolicies.ts new file mode 100644 index 00000000..cceb4606 --- /dev/null +++ b/src/api/plugins/evaluatePolicies.ts @@ -0,0 +1,74 @@ +import fp from "fastify-plugin"; +import { FastifyPluginAsync, FastifyRequest } from "fastify"; +import { UnauthorizedError } from "common/errors/index.js"; +import { + AuthorizationPoliciesRegistry, + AvailableAuthorizationPolicies, +} from "common/policies/definition.js"; +import { evaluatePolicy } from "common/policies/evaluator.js"; + +/** + * Evaluates all policy restrictions for a request + * @param {FastifyRequest} request - The Fastify request object + * @returns {Promise} - True if all policies pass, throws error otherwise + */ +export const evaluateAllRequestPolicies = async ( + request: FastifyRequest, +): Promise => { + if (!request.policyRestrictions) { + return true; + } + + for (const restriction of request.policyRestrictions) { + if ( + AuthorizationPoliciesRegistry[ + restriction.name as keyof AvailableAuthorizationPolicies + ] === undefined + ) { + request.log.warn(`Invalid policy name ${restriction.name}, skipping...`); + continue; + } + + const policyFunction = + AuthorizationPoliciesRegistry[ + restriction.name as keyof AvailableAuthorizationPolicies + ]; + const policyResult = evaluatePolicy(request, { + policy: policyFunction, + params: restriction.params, + }); + + request.log.info( + `Policy ${restriction.name} evaluated to ${policyResult.allowed}.`, + ); + + if (!policyResult.allowed) { + return policyResult.message; + } + } + + return true; +}; + +/** + * Fastify plugin to evaluate authorization policies after the request body has been parsed + */ +const evaluatePoliciesPluginAsync: FastifyPluginAsync = async ( + fastify, + _options, +) => { + // Register a hook that runs after body parsing but before route handler + fastify.addHook("preHandler", async (request: FastifyRequest, _reply) => { + const result = await evaluateAllRequestPolicies(request); + if (typeof result === "string") { + throw new UnauthorizedError({ + message: result, + }); + } + }); +}; + +// Export the plugin as a properly wrapped fastify-plugin +export default fp(evaluatePoliciesPluginAsync, { + name: "evaluatePolicies", +}); diff --git a/src/api/plugins/validate.ts b/src/api/plugins/validate.ts deleted file mode 100644 index cf948b7e..00000000 --- a/src/api/plugins/validate.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { FastifyPluginAsync, FastifyReply, FastifyRequest } from "fastify"; -import fp from "fastify-plugin"; -import { - InternalServerError, - ValidationError, -} from "../../common/errors/index.js"; -import { z, ZodError } from "zod"; -import { fromError } from "zod-validation-error"; - -const zodValidationPlugin: FastifyPluginAsync = async (fastify, _options) => { - fastify.decorate( - "zodValidateBody", - async function ( - request: FastifyRequest, - _reply: FastifyReply, - zodSchema: z.ZodTypeAny, - ): Promise { - try { - await zodSchema.parseAsync(request.body || {}); - } catch (e: unknown) { - if (e instanceof ZodError) { - throw new ValidationError({ - message: fromError(e).toString().replace("Validation error: ", ""), - }); - } else if (e instanceof Error) { - request.log.error(`Error validating request body: ${e.toString()}`); - throw new InternalServerError({ - message: "Could not validate request body.", - }); - } - throw e; - } - }, - ); -}; - -const fastifyZodValidationPlugin = fp(zodValidationPlugin); -export default fastifyZodValidationPlugin; diff --git a/src/api/public/404.html b/src/api/public/404.html new file mode 100644 index 00000000..d7eef165 --- /dev/null +++ b/src/api/public/404.html @@ -0,0 +1,65 @@ + + + + + + + Page Not Found | ACM @ UIUC + + + + + +
+

404

+

The page you are looking for could not be found.

+
© ACM @ UIUC
+
+ + + \ No newline at end of file diff --git a/src/api/routes/apiKey.ts b/src/api/routes/apiKey.ts new file mode 100644 index 00000000..36288745 --- /dev/null +++ b/src/api/routes/apiKey.ts @@ -0,0 +1,202 @@ +import { FastifyPluginAsync } from "fastify"; +import rateLimiter from "api/plugins/rateLimiter.js"; +import { withRoles, withTags } from "api/components/index.js"; +import { AppRoles } from "common/roles.js"; +import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi"; +import { apiKeyPostBody } from "common/types/apiKey.js"; +import { createApiKey } from "api/functions/apiKey.js"; +import { buildAuditLogTransactPut } from "api/functions/auditLog.js"; +import { Modules } from "common/modules.js"; +import { genericConfig } from "common/config.js"; +import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; +import { + ConditionalCheckFailedException, + ScanCommand, + TransactWriteItemsCommand, +} from "@aws-sdk/client-dynamodb"; +import { + BaseError, + DatabaseDeleteError, + DatabaseFetchError, + DatabaseInsertError, + ValidationError, +} from "common/errors/index.js"; +import { z } from "zod"; +import { ApiKeyDynamoEntry } from "api/functions/apiKey.js"; + +const apiKeyRoute: FastifyPluginAsync = async (fastify, _options) => { + await fastify.register(rateLimiter, { + limit: 15, + duration: 30, + rateLimitIdentifier: "apiKey", + }); + fastify.withTypeProvider().post( + "/org", + { + schema: withRoles( + [AppRoles.MANAGE_ORG_API_KEYS], + withTags(["API Keys"], { + summary: "Create an organization API key.", + body: apiKeyPostBody, + }), + { disableApiKeyAuth: true }, + ), + onRequest: fastify.authorizeFromSchema, + }, + async (request, reply) => { + const { roles, description, expiresAt } = request.body; + const { apiKey, hashedKey, keyId } = await createApiKey(); + const logStatement = buildAuditLogTransactPut({ + entry: { + module: Modules.API_KEY, + message: `Created API key.`, + actor: request.username!, + target: `acmuiuc_${keyId}`, + requestId: request.id, + }, + }); + const apiKeyPayload: ApiKeyDynamoEntry = { + keyId, + keyHash: hashedKey, + roles, + description, + owner: request.username!, + createdAt: Math.floor(Date.now() / 1000), + ...(expiresAt ? { expiresAt } : {}), + }; + const command = new TransactWriteItemsCommand({ + TransactItems: [ + logStatement, + { + Put: { + TableName: genericConfig.ApiKeyTable, + Item: marshall(apiKeyPayload), + ConditionExpression: "attribute_not_exists(keyId)", + }, + }, + ], + }); + try { + await fastify.dynamoClient.send(command); + } catch (e) { + if (e instanceof BaseError) { + throw e; + } + fastify.log.error(e); + throw new DatabaseInsertError({ + message: "Could not create API key.", + }); + } + return reply.status(201).send({ + apiKey, + expiresAt, + }); + }, + ); + fastify.withTypeProvider().delete( + "/org/:keyId", + { + schema: withRoles( + [AppRoles.MANAGE_ORG_API_KEYS], + withTags(["API Keys"], { + summary: "Delete an organization API key.", + params: z.object({ + keyId: z.string().min(1).openapi({ + description: + "Key ID to delete. The key ID is the second segment of the API key.", + }), + }), + }), + { disableApiKeyAuth: true }, + ), + onRequest: fastify.authorizeFromSchema, + }, + async (request, reply) => { + const { keyId } = request.params; + const logStatement = buildAuditLogTransactPut({ + entry: { + module: Modules.API_KEY, + message: `Deleted API key.`, + actor: request.username!, + target: `acmuiuc_${keyId}`, + requestId: request.id, + }, + }); + const command = new TransactWriteItemsCommand({ + TransactItems: [ + logStatement, + { + Delete: { + TableName: genericConfig.ApiKeyTable, + Key: { keyId: { S: keyId } }, + ConditionExpression: "attribute_exists(keyId)", + }, + }, + ], + }); + try { + await fastify.dynamoClient.send(command); + } catch (e) { + if (e instanceof BaseError) { + throw e; + } + if (e instanceof ConditionalCheckFailedException) { + throw new ValidationError({ + message: "Key does not exist.", + }); + } + fastify.log.error(e); + throw new DatabaseDeleteError({ + message: "Could not delete API key.", + }); + } + return reply.status(204).send(); + }, + ); + fastify.withTypeProvider().get( + "/org", + { + schema: withRoles( + [AppRoles.MANAGE_ORG_API_KEYS], + withTags(["API Keys"], { + summary: "Get all organization API keys.", + }), + { disableApiKeyAuth: true }, + ), + onRequest: fastify.authorizeFromSchema, + }, + async (request, reply) => { + const command = new ScanCommand({ + TableName: genericConfig.ApiKeyTable, + }); + try { + const result = await fastify.dynamoClient.send(command); + if (!result || !result.Items) { + throw new DatabaseFetchError({ + message: "Could not fetch API keys.", + }); + } + const unmarshalled = result.Items.map((x) => + unmarshall(x), + ) as ApiKeyDynamoEntry[]; + const filtered = unmarshalled + .map((x) => ({ + ...x, + keyHash: undefined, + })) + .filter((x) => !x.expiresAt || x.expiresAt < Date.now()); + return reply.status(200).send(filtered); + } catch (e) { + if (e instanceof BaseError) { + throw e; + } + fastify.log.error(e); + throw new DatabaseFetchError({ + message: "Could not fetch API keys.", + }); + } + }, + ); +}; + +export default apiKeyRoute; diff --git a/src/api/routes/events.ts b/src/api/routes/events.ts index f21400a2..cc874a43 100644 --- a/src/api/routes/events.ts +++ b/src/api/routes/events.ts @@ -1,3 +1,4 @@ +import "zod-openapi/extend"; import { FastifyPluginAsync, FastifyRequest } from "fastify"; import { AppRoles } from "../../common/roles.js"; import { z } from "zod"; @@ -17,7 +18,10 @@ import { DatabaseFetchError, DatabaseInsertError, DiscordEventError, + InternalServerError, NotFoundError, + UnauthenticatedError, + UnauthorizedError, ValidationError, } from "../../common/errors/index.js"; import { randomUUID } from "crypto"; @@ -29,21 +33,90 @@ import { deleteCacheCounter, getCacheCounter, } from "api/functions/cache.js"; +import { createAuditLogEntry } from "api/functions/auditLog.js"; +import { Modules } from "common/modules.js"; +import { + FastifyPluginAsyncZodOpenApi, + FastifyZodOpenApiSchema, + FastifyZodOpenApiTypeProvider, +} from "fastify-zod-openapi"; +import { ts, withRoles, withTags } from "api/components/index.js"; +import { metadataSchema } from "common/types/events.js"; +import { evaluateAllRequestPolicies } from "api/plugins/evaluatePolicies.js"; + +const createProjectionParams = (includeMetadata: boolean = false) => { + // Object mapping attribute names to their expression aliases + const attributeMapping = { + title: "#title", + description: "#description", + start: "#startTime", // Reserved keyword + end: "#endTime", // Potential reserved keyword + location: "#location", + locationLink: "#locationLink", + host: "#host", + featured: "#featured", + id: "#id", + ...(includeMetadata ? { metadata: "#metadata" } : {}), + }; + + // Create expression attribute names object for DynamoDB + const expressionAttributeNames = Object.entries(attributeMapping).reduce( + (acc, [attrName, exprName]) => { + acc[exprName] = attrName; + return acc; + }, + {} as { [key: string]: string }, + ); + + // Create projection expression from the values of attributeMapping + const projectionExpression = Object.values(attributeMapping).join(","); + + return { + attributeMapping, + expressionAttributeNames, + projectionExpression, + // Return function to destructure results if needed + getAttributes: (item: any): T => item as T, + }; +}; const repeatOptions = ["weekly", "biweekly"] as const; -const CLIENT_HTTP_CACHE_POLICY = `public, max-age=${EVENT_CACHED_DURATION}, stale-while-revalidate=420, stale-if-error=3600`; +const zodIncludeMetadata = z.coerce + .boolean() + .default(false) + .optional() + .openapi({ + description: "If true, include metadata for each event entry.", + }); +export const CLIENT_HTTP_CACHE_POLICY = `public, max-age=${EVENT_CACHED_DURATION}, stale-while-revalidate=420, stale-if-error=3600`; export type EventRepeatOptions = (typeof repeatOptions)[number]; const baseSchema = z.object({ title: z.string().min(1), description: z.string().min(1), - start: z.string(), - end: z.optional(z.string()), - location: z.string(), - locationLink: z.optional(z.string().url()), + start: z.string().openapi({ + description: "Timestamp in the America/Chicago timezone.", + example: "2024-08-27T19:00:00", + }), + end: z.optional(z.string()).openapi({ + description: "Timestamp in the America/Chicago timezone.", + example: "2024-08-27T20:00:00", + }), + location: z.string().openapi({ + description: "Human-friendly location name.", + example: "Siebel Center for Computer Science", + }), + locationLink: z.optional(z.string().url()).openapi({ + description: "Google Maps link for easy navigation to the event location.", + example: "https://maps.app.goo.gl/dwbBBBkfjkgj8gvA8", + }), host: z.enum(OrganizationList as [string, ...string[]]), - featured: z.boolean().default(false), + featured: z.boolean().default(false).openapi({ + description: + "Whether or not the event should be shown on the ACM @ UIUC website home page (and added to Discord, as available).", + }), paidEventId: z.optional(z.string().min(1)), + metadata: metadataSchema, }); const requestSchema = baseSchema.extend({ @@ -51,80 +124,65 @@ const requestSchema = baseSchema.extend({ repeatEnds: z.string().optional(), }); -// eslint-disable-next-line @typescript-eslint/no-unused-vars const postRequestSchema = requestSchema.refine( (data) => (data.repeatEnds ? data.repeats !== undefined : true), { message: "repeats is required when repeatEnds is defined", }, ); - export type EventPostRequest = z.infer; -type EventGetRequest = { - Params: { id: string }; - Querystring: { ts?: number }; - Body: undefined; -}; - -type EventDeleteRequest = { - Params: { id: string }; - Querystring: undefined; - Body: undefined; -}; - -const responseJsonSchema = zodToJsonSchema( - z.object({ - id: z.string(), - resource: z.string(), - }), -); -// GET const getEventSchema = requestSchema.extend({ id: z.string(), }); - export type EventGetResponse = z.infer; -const getEventJsonSchema = zodToJsonSchema(getEventSchema); const getEventsSchema = z.array(getEventSchema); export type EventsGetResponse = z.infer; -type EventsGetRequest = { - Body: undefined; - Querystring?: { - upcomingOnly?: boolean; - host?: string; - ts?: number; - }; -}; -const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => { +const eventsPlugin: FastifyPluginAsyncZodOpenApi = async ( + fastify, + _options, +) => { const limitedRoutes: FastifyPluginAsync = async (fastify) => { fastify.register(rateLimiter, { limit: 30, duration: 60, rateLimitIdentifier: "events", }); - fastify.get( - "/", + fastify.withTypeProvider().get( + "", { - schema: { - querystring: { - type: "object", - properties: { - upcomingOnly: { type: "boolean" }, - host: { type: "string" }, - ts: { type: "number" }, - }, - }, - response: { 200: getEventsSchema }, - }, + schema: withTags(["Events"], { + querystring: z.object({ + upcomingOnly: z.coerce.boolean().default(false).optional().openapi({ + description: + "If true, only get events which have at least one occurance starting after the current time.", + }), + featuredOnly: z.coerce.boolean().default(false).optional().openapi({ + description: + "If true, only get events which are marked as featured.", + }), + host: z + .enum(OrganizationList as [string, ...string[]]) + .optional() + .openapi({ + description: "Retrieve events only for a specific host.", + }), + ts, + includeMetadata: zodIncludeMetadata, + }), + summary: "Retrieve calendar events with applied filters.", + // response: { 200: getEventsSchema }, + }), }, - async (request: FastifyRequest, reply) => { + async (request, reply) => { const upcomingOnly = request.query?.upcomingOnly || false; + const featuredOnly = request.query?.featuredOnly || false; + const includeMetadata = request.query.includeMetadata || true; const host = request.query?.host; const ts = request.query?.ts; // we only use this to disable cache control - + const projection = createProjectionParams(includeMetadata); try { const ifNoneMatch = request.headers["if-none-match"]; if (ifNoneMatch) { @@ -156,10 +214,14 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => { }, KeyConditionExpression: "host = :host", IndexName: "HostIndex", + ProjectionExpression: projection.projectionExpression, + ExpressionAttributeNames: projection.expressionAttributeNames, }); } else { command = new ScanCommand({ TableName: genericConfig.EventsDynamoTableName, + ProjectionExpression: projection.projectionExpression, + ExpressionAttributeNames: projection.expressionAttributeNames, }); } if (!ifNoneMatch) { @@ -204,6 +266,9 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => { } }); } + if (featuredOnly) { + parsedItems = parsedItems.filter((x) => x.featured); + } if (!ts) { reply.header("Cache-Control", CLIENT_HTTP_CACHE_POLICY); } @@ -222,25 +287,38 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => { ); }; - fastify.post<{ Body: EventPostRequest }>( + fastify.withTypeProvider().post( "/:id?", { - schema: { - response: { 201: responseJsonSchema }, - }, - preValidation: async (request, reply) => { - await fastify.zodValidateBody(request, reply, postRequestSchema); - }, - onRequest: async (request, reply) => { - await fastify.authorize(request, reply, [AppRoles.EVENTS_MANAGER]); - }, + schema: withRoles( + [AppRoles.EVENTS_MANAGER], + withTags(["Events"], { + // response: { + // 201: z.object({ + // id: z.string(), + // resource: z.string(), + // }), + // }, + body: postRequestSchema, + params: z.object({ + id: z.string().min(1).optional().openapi({ + description: + "Event ID to modify (leave empty to create a new event).", + example: "6667e095-8b04-4877-b361-f636f459ba42", + }), + }), + summary: "Modify a calendar event.", + }), + ) satisfies FastifyZodOpenApiSchema, + onRequest: fastify.authorizeFromSchema, }, async (request, reply) => { + if (!request.username) { + throw new UnauthenticatedError({ message: "Username not found." }); + } try { let originalEvent; - const userProvidedId = ( - request.params as Record - ).id; + const userProvidedId = request.params.id; const entryUUID = userProvidedId || randomUUID(); if (userProvidedId) { const response = await fastify.dynamoClient.send( @@ -257,6 +335,10 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => { } originalEvent = unmarshall(originalEvent); } + let verb = "created"; + if (userProvidedId && userProvidedId === entryUUID) { + verb = "modified"; + } const entry = { ...request.body, id: entryUUID, @@ -272,15 +354,12 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => { Item: marshall(entry), }), ); - let verb = "created"; - if (userProvidedId && userProvidedId === entryUUID) { - verb = "modified"; - } try { if (request.body.featured && !request.body.repeats) { await updateDiscord( fastify.secretsManagerClient, entry, + request.username, false, request.log, ); @@ -322,19 +401,20 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => { 1, false, ); + await createAuditLogEntry({ + dynamoClient: fastify.dynamoClient, + entry: { + module: Modules.EVENTS, + actor: request.username, + target: entryUUID, + message: `${verb} event "${entryUUID}"`, + requestId: request.id, + }, + }); reply.status(201).send({ id: entryUUID, resource: `/api/v1/events/${entryUUID}`, }); - request.log.info( - { - type: "audit", - module: "events", - actor: request.username, - target: entryUUID, - }, - `${verb} event "${entryUUID}"`, - ); } catch (e: unknown) { if (e instanceof Error) { request.log.error("Failed to insert to DynamoDB: " + e.toString()); @@ -348,18 +428,65 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => { } }, ); - fastify.delete( + fastify.withTypeProvider().delete( "/:id", { - schema: { - response: { 201: responseJsonSchema }, - }, - onRequest: async (request, reply) => { - await fastify.authorize(request, reply, [AppRoles.EVENTS_MANAGER]); + schema: withRoles( + [AppRoles.EVENTS_MANAGER], + withTags(["Events"], { + params: z.object({ + id: z.string().min(1).openapi({ + description: "Event ID to delete.", + example: "6667e095-8b04-4877-b361-f636f459ba42", + }), + }), + // response: { + // 201: z.object({ + // id: z.string(), + // resource: z.string(), + // }), + // }, + summary: "Delete a calendar event.", + }), + ) satisfies FastifyZodOpenApiSchema, + onRequest: fastify.authorizeFromSchema, + preHandler: async (request, reply) => { + if (request.policyRestrictions) { + const response = await fastify.dynamoClient.send( + new GetItemCommand({ + TableName: genericConfig.EventsDynamoTableName, + Key: marshall({ id: request.params.id }), + }), + ); + const item = response.Item ? unmarshall(response.Item) : null; + if (!item) { + return reply.status(204).send(); + } + const fakeBody = { ...request, body: item, url: request.url }; + try { + const result = await evaluateAllRequestPolicies(fakeBody); + if (typeof result === "string") { + throw new UnauthorizedError({ + message: result, + }); + } + } catch (err) { + if (err instanceof BaseError) { + throw err; + } + fastify.log.error(err); + throw new InternalServerError({ + message: "Failed to evaluate policies.", + }); + } + } }, }, - async (request: FastifyRequest, reply) => { + async (request, reply) => { const id = request.params.id; + if (!request.username) { + throw new UnauthenticatedError({ message: "Username not found." }); + } try { await fastify.dynamoClient.send( new DeleteItemCommand({ @@ -370,12 +497,20 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => { await updateDiscord( fastify.secretsManagerClient, { id } as IUpdateDiscord, + request.username, true, request.log, ); - reply.status(201).send({ - id, - resource: `/api/v1/events/${id}`, + reply.status(204).send(); + await createAuditLogEntry({ + dynamoClient: fastify.dynamoClient, + entry: { + module: Modules.EVENTS, + actor: request.username, + target: id, + message: `Deleted event "${id}"`, + requestId: request.id, + }, }); } catch (e: unknown) { if (e instanceof Error) { @@ -392,33 +527,30 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => { 1, false, ); - request.log.info( - { - type: "audit", - module: "events", - actor: request.username, - target: id, - }, - `deleted event "${id}"`, - ); }, ); - fastify.get( + fastify.withTypeProvider().get( "/:id", { - schema: { - querystring: { - type: "object", - properties: { - ts: { type: "number" }, - }, - }, - response: { 200: getEventJsonSchema }, - }, + schema: withTags(["Events"], { + params: z.object({ + id: z.string().min(1).openapi({ + description: "Event ID to delete.", + example: "6667e095-8b04-4877-b361-f636f459ba42", + }), + }), + querystring: z.object({ + ts, + includeMetadata: zodIncludeMetadata, + }), + summary: "Retrieve a calendar event.", + // response: { 200: getEventSchema }, + }), }, - async (request: FastifyRequest, reply) => { + async (request, reply) => { const id = request.params.id; const ts = request.query?.ts; + const includeMetadata = request.query?.includeMetadata || false; try { // Check If-None-Match header @@ -442,11 +574,13 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => { reply.header("etag", etag); } - + const projection = createProjectionParams(includeMetadata); const response = await fastify.dynamoClient.send( new GetItemCommand({ TableName: genericConfig.EventsDynamoTableName, Key: marshall({ id }), + ProjectionExpression: projection.projectionExpression, + ExpressionAttributeNames: projection.expressionAttributeNames, }), ); const item = response.Item ? unmarshall(response.Item) : null; @@ -467,11 +601,12 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => { reply.header("etag", etag); } - return reply.send(item); + return reply.send(item as z.infer); } catch (e) { if (e instanceof BaseError) { throw e; } + fastify.log.error(e); throw new DatabaseFetchError({ message: "Failed to get event from Dynamo table.", }); diff --git a/src/api/routes/iam.ts b/src/api/routes/iam.ts index b390bd1c..0f6d9725 100644 --- a/src/api/routes/iam.ts +++ b/src/api/routes/iam.ts @@ -1,5 +1,5 @@ import { FastifyPluginAsync } from "fastify"; -import { allAppRoles, AppRoles } from "../../common/roles.js"; +import { AppRoles } from "../../common/roles.js"; import { zodToJsonSchema } from "zod-to-json-schema"; import { addToTenant, @@ -16,22 +16,17 @@ import { EntraInvitationError, InternalServerError, NotFoundError, - UnauthorizedError, } from "../../common/errors/index.js"; import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb"; import { genericConfig, roleArns } from "../../common/config.js"; import { marshall } from "@aws-sdk/util-dynamodb"; import { - InviteUserPostRequest, invitePostRequestSchema, - GroupMappingCreatePostRequest, groupMappingCreatePostSchema, entraActionResponseSchema, groupModificationPatchSchema, - GroupModificationPatchRequest, EntraGroupActions, entraGroupMembershipListResponse, - ProfilePatchRequest, entraProfilePatchRequest, } from "../../common/types/iam.js"; import { @@ -40,6 +35,15 @@ import { } from "../functions/authorization.js"; import { getRoleCredentials } from "api/functions/sts.js"; import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; +import { createAuditLogEntry } from "api/functions/auditLog.js"; +import { Modules } from "common/modules.js"; +import { groupId, withRoles, withTags } from "api/components/index.js"; +import { + FastifyZodOpenApiTypeProvider, + serializerCompiler, + validatorCompiler, +} from "fastify-zod-openapi"; +import { z } from "zod"; const iamRoutes: FastifyPluginAsync = async (fastify, _options) => { const getAuthorizedClients = async () => { @@ -72,15 +76,17 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => { }; } }; - fastify.patch<{ Body: ProfilePatchRequest }>( + fastify.withTypeProvider().patch( "/profile", { - preValidation: async (request, reply) => { - await fastify.zodValidateBody(request, reply, entraProfilePatchRequest); - }, - onRequest: async (request, reply) => { - await fastify.authorize(request, reply, []); - }, + schema: withRoles( + [], + withTags(["IAM"], { + body: entraProfilePatchRequest, + summary: "Update user's profile.", + }), + ), + onRequest: fastify.authorizeFromSchema, }, async (request, reply) => { if (!request.tokenPayload || !request.username) { @@ -99,32 +105,26 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => { userOid, request.body, ); - reply.status(201); + reply.status(201).send(); }, ); - fastify.get<{ - Body: undefined; - Querystring: { groupId: string }; - }>( + fastify.withTypeProvider().get( "/groups/:groupId/roles", { - schema: { - querystring: { - type: "object", - properties: { - groupId: { - type: "string", - }, - }, - }, - }, - onRequest: async (request, reply) => { - await fastify.authorize(request, reply, [AppRoles.IAM_ADMIN]); - }, + schema: withRoles( + [AppRoles.IAM_ADMIN], + withTags(["IAM"], { + params: z.object({ + groupId, + }), + summary: "Get a group's application role mappings.", + }), + ), + onRequest: fastify.authorizeFromSchema, }, async (request, reply) => { try { - const groupId = (request.params as Record).groupId; + const groupId = request.params.groupId; const roles = await getGroupRoles( fastify.dynamoClient, fastify, @@ -143,32 +143,20 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => { } }, ); - fastify.post<{ - Body: GroupMappingCreatePostRequest; - Querystring: { groupId: string }; - }>( + fastify.withTypeProvider().post( "/groups/:groupId/roles", { - schema: { - querystring: { - type: "object", - properties: { - groupId: { - type: "string", - }, - }, - }, - }, - preValidation: async (request, reply) => { - await fastify.zodValidateBody( - request, - reply, - groupMappingCreatePostSchema, - ); - }, - onRequest: async (request, reply) => { - await fastify.authorize(request, reply, [AppRoles.IAM_ADMIN]); - }, + schema: withRoles( + [AppRoles.IAM_ADMIN], + withTags(["IAM"], { + params: z.object({ + groupId, + }), + body: groupMappingCreatePostSchema, + summary: "Update a group's application role mappings.", + }), + ), + onRequest: fastify.authorizeFromSchema, }, async (request, reply) => { const groupId = (request.params as Record).groupId; @@ -182,7 +170,18 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => { createdAt: timestamp, }), }); + const logPromise = createAuditLogEntry({ + dynamoClient: fastify.dynamoClient, + entry: { + module: Modules.IAM, + actor: request.username!, + target: groupId, + message: `set target roles to ${request.body.roles.toString()}`, + requestId: request.id, + }, + }); await fastify.dynamoClient.send(command); + await logPromise; fastify.nodeCache.set( `grouproles-${groupId}`, request.body.roles, @@ -200,29 +199,20 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => { }); } reply.send({ message: "OK" }); - request.log.info( - { - type: "audit", - module: "iam", - actor: request.username, - target: groupId, - }, - `set target roles to ${request.body.roles.toString()}`, - ); }, ); - fastify.post<{ Body: InviteUserPostRequest }>( + fastify.withTypeProvider().post( "/inviteUsers", { - schema: { - response: { 202: zodToJsonSchema(entraActionResponseSchema) }, - }, - preValidation: async (request, reply) => { - await fastify.zodValidateBody(request, reply, invitePostRequestSchema); - }, - onRequest: async (request, reply) => { - await fastify.authorize(request, reply, [AppRoles.IAM_INVITE_ONLY]); - }, + schema: withRoles( + [AppRoles.IAM_INVITE_ONLY, AppRoles.IAM_ADMIN], + withTags(["IAM"], { + body: invitePostRequestSchema, + summary: "Invite a user to the ACM @ UIUC Entra ID tenant.", + // response: { 202: entraActionResponseSchema }, + }), + ), + onRequest: fastify.authorizeFromSchema, }, async (request, reply) => { const emails = request.body.emails; @@ -242,28 +232,35 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => { const results = await Promise.allSettled( emails.map((email) => addToTenant(entraIdToken, email)), ); + const logPromises = []; for (let i = 0; i < results.length; i++) { const result = results[i]; if (result.status === "fulfilled") { - request.log.info( - { - type: "audit", - module: "iam", - actor: request.username, - target: emails[i], - }, - "invited user to Entra ID tenant.", + logPromises.push( + createAuditLogEntry({ + dynamoClient: fastify.dynamoClient, + entry: { + module: Modules.IAM, + actor: request.username!, + target: emails[i], + message: "Invited user to Entra ID tenant.", + requestId: request.id, + }, + }), ); response.success.push({ email: emails[i] }); } else { - request.log.info( - { - type: "audit", - module: "iam", - actor: request.username, - target: emails[i], - }, - "failed to invite user to Entra ID tenant.", + logPromises.push( + createAuditLogEntry({ + dynamoClient: fastify.dynamoClient, + entry: { + module: Modules.IAM, + actor: request.username!, + target: emails[i], + message: "Failed to invite user to Entra ID tenant.", + requestId: request.id, + }, + }), ); if (result.reason instanceof EntraInvitationError) { response.failure.push({ @@ -278,35 +275,24 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => { } } } + await Promise.allSettled(logPromises); reply.status(202).send(response); }, ); - fastify.patch<{ - Body: GroupModificationPatchRequest; - Querystring: { groupId: string }; - }>( + fastify.withTypeProvider().patch( "/groups/:groupId", { - schema: { - querystring: { - type: "object", - properties: { - groupId: { - type: "string", - }, - }, - }, - }, - preValidation: async (request, reply) => { - await fastify.zodValidateBody( - request, - reply, - groupModificationPatchSchema, - ); - }, - onRequest: async (request, reply) => { - await fastify.authorize(request, reply, [AppRoles.IAM_ADMIN]); - }, + schema: withRoles( + [AppRoles.IAM_ADMIN], + withTags(["IAM"], { + params: z.object({ + groupId, + }), + body: groupModificationPatchSchema, + summary: "Update the members of a group.", + }), + ), + onRequest: fastify.authorizeFromSchema, }, async (request, reply) => { const groupId = (request.params as Record).groupId; @@ -353,28 +339,35 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => { success: [], failure: [], }; + const logPromises = []; for (let i = 0; i < addResults.length; i++) { const result = addResults[i]; if (result.status === "fulfilled") { response.success.push({ email: request.body.add[i] }); - request.log.info( - { - type: "audit", - module: "iam", - actor: request.username, - target: request.body.add[i], - }, - `added target to group ID ${groupId}`, + logPromises.push( + createAuditLogEntry({ + dynamoClient: fastify.dynamoClient, + entry: { + module: Modules.IAM, + actor: request.username!, + target: request.body.add[i], + message: `added target to group ID ${groupId}`, + requestId: request.id, + }, + }), ); } else { - request.log.info( - { - type: "audit", - module: "iam", - actor: request.username, - target: request.body.add[i], - }, - `failed to add target to group ID ${groupId}`, + logPromises.push( + createAuditLogEntry({ + dynamoClient: fastify.dynamoClient, + entry: { + module: Modules.IAM, + actor: request.username!, + target: request.body.add[i], + message: `failed to add target to group ID ${groupId}`, + requestId: request.id, + }, + }), ); if (result.reason instanceof EntraGroupError) { response.failure.push({ @@ -393,24 +386,30 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => { const result = removeResults[i]; if (result.status === "fulfilled") { response.success.push({ email: request.body.remove[i] }); - request.log.info( - { - type: "audit", - module: "iam", - actor: request.username, - target: request.body.remove[i], - }, - `removed target from group ID ${groupId}`, + logPromises.push( + createAuditLogEntry({ + dynamoClient: fastify.dynamoClient, + entry: { + module: Modules.IAM, + actor: request.username!, + target: request.body.add[i], + message: `remove target from group ID ${groupId}`, + requestId: request.id, + }, + }), ); } else { - request.log.info( - { - type: "audit", - module: "iam", - actor: request.username, - target: request.body.add[i], - }, - `failed to remove target from group ID ${groupId}`, + logPromises.push( + createAuditLogEntry({ + dynamoClient: fastify.dynamoClient, + entry: { + module: Modules.IAM, + actor: request.username!, + target: request.body.add[i], + message: `failed to remove target from group ID ${groupId}`, + requestId: request.id, + }, + }), ); if (result.reason instanceof EntraGroupError) { response.failure.push({ @@ -425,28 +424,24 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => { } } } + await Promise.allSettled(logPromises); reply.status(202).send(response); }, ); - fastify.get<{ - Querystring: { groupId: string }; - }>( + fastify.withTypeProvider().get( "/groups/:groupId", { - schema: { - response: { 200: zodToJsonSchema(entraGroupMembershipListResponse) }, - querystring: { - type: "object", - properties: { - groupId: { - type: "string", - }, - }, - }, - }, - onRequest: async (request, reply) => { - await fastify.authorize(request, reply, [AppRoles.IAM_ADMIN]); - }, + schema: withRoles( + [AppRoles.IAM_ADMIN], + withTags(["IAM"], { + // response: { 200: entraGroupMembershipListResponse }, + params: z.object({ + groupId, + }), + summary: "Get the members of a group.", + }), + ), + onRequest: fastify.authorizeFromSchema, }, async (request, reply) => { const groupId = (request.params as Record).groupId; @@ -465,7 +460,9 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => { } const entraIdToken = await getEntraIdToken( await getAuthorizedClients(), - fastify.environmentConfig.AadValidClientId, + fastify.environmentConfig.AadValidReadOnlyClientId, + undefined, + genericConfig.EntraReadOnlySecretName, ); const response = await listGroupMembers(entraIdToken, groupId); reply.status(200).send(response); diff --git a/src/api/routes/ics.ts b/src/api/routes/ics.ts index bf3eab86..ef3a5eaa 100644 --- a/src/api/routes/ics.ts +++ b/src/api/routes/ics.ts @@ -15,8 +15,15 @@ import ical, { import moment from "moment"; import { getVtimezoneComponent } from "@touch4it/ical-timezones"; import { OrganizationList } from "../../common/orgs.js"; -import { EventRepeatOptions } from "./events.js"; +import { CLIENT_HTTP_CACHE_POLICY, EventRepeatOptions } from "./events.js"; import rateLimiter from "api/plugins/rateLimiter.js"; +import { getCacheCounter } from "api/functions/cache.js"; +import { + FastifyZodOpenApiSchema, + FastifyZodOpenApiTypeProvider, +} from "fastify-zod-openapi"; +import { withTags } from "api/components/index.js"; +import { z } from "zod"; const repeatingIcalMap: Record = { @@ -40,103 +47,139 @@ const icalPlugin: FastifyPluginAsync = async (fastify, _options) => { duration: 30, rateLimitIdentifier: "ical", }); - fastify.get("/:host?", async (request, reply) => { - const host = (request.params as Record).host; - let queryParams: QueryCommandInput = { - TableName: genericConfig.EventsDynamoTableName, - }; - let response; - if (host) { - if (!OrganizationList.includes(host)) { - throw new ValidationError({ - message: `Invalid host parameter "${host}" in path.`, - }); - } - queryParams = { - ...queryParams, + fastify.withTypeProvider().get( + "/:host?", + { + schema: withTags(["iCalendar Integration"], { + params: z.object({ + host: z + .optional(z.enum(OrganizationList as [string, ...string[]])) + .openapi({ description: "Host to get calendar for." }), + }), + summary: + "Retrieve the calendar for ACM @ UIUC or a specific sub-organization.", + } satisfies FastifyZodOpenApiSchema), + }, + async (request, reply) => { + const host = request.params.host; + let queryParams: QueryCommandInput = { + TableName: genericConfig.EventsDynamoTableName, }; - response = await fastify.dynamoClient.send( - new QueryCommand({ + let response; + const ifNoneMatch = request.headers["if-none-match"]; + if (ifNoneMatch) { + const etag = await getCacheCounter( + fastify.dynamoClient, + `events-etag-${host || "all"}`, + ); + + if ( + ifNoneMatch === `"${etag.toString()}"` || + ifNoneMatch === etag.toString() + ) { + return reply + .code(304) + .header("ETag", etag) + .header("Cache-Control", CLIENT_HTTP_CACHE_POLICY) + .send(); + } + + reply.header("etag", etag); + } + if (host) { + if (!OrganizationList.includes(host)) { + throw new ValidationError({ + message: `Invalid host parameter "${host}" in path.`, + }); + } + queryParams = { ...queryParams, - ExpressionAttributeValues: { - ":host": { - S: host, + }; + response = await fastify.dynamoClient.send( + new QueryCommand({ + ...queryParams, + ExpressionAttributeValues: { + ":host": { + S: host, + }, }, - }, - KeyConditionExpression: "host = :host", - IndexName: "HostIndex", - }), - ); - } else { - response = await fastify.dynamoClient.send(new ScanCommand(queryParams)); - } - const dynamoItems = response.Items - ? response.Items.map((x) => unmarshall(x)) - : null; - if (!dynamoItems) { - throw new NotFoundError({ - endpointName: host ? `/api/v1/ical/${host}` : "/api/v1/ical", - }); - } - // generate friendly calendar name - let calendarName = - host && host.includes("ACM") - ? `${host} Events` - : `ACM@UIUC - ${host} Events`; - if (host == "ACM") { - calendarName = "ACM@UIUC - Major Events"; - } - if (!host) { - calendarName = "ACM@UIUC - All Events"; - } - const calendar = ical({ name: calendarName }); - calendar.timezone({ - name: "America/Chicago", - generator: getVtimezoneComponent, - }); - calendar.method(ICalCalendarMethod.PUBLISH); - for (const rawEvent of dynamoItems) { - let event = calendar.createEvent({ - start: moment.tz(rawEvent.start, "America/Chicago"), - end: rawEvent.end - ? moment.tz(rawEvent.end, "America/Chicago") - : moment.tz(rawEvent.start, "America/Chicago"), - summary: rawEvent.title, - description: rawEvent.locationLink - ? `Host: ${rawEvent.host}\nGoogle Maps Link: ${rawEvent.locationLink}\n\n` + - rawEvent.description - : `Host: ${rawEvent.host}\n\n` + rawEvent.description, - timezone: "America/Chicago", - organizer: generateHostName(host), - id: rawEvent.id, + KeyConditionExpression: "host = :host", + IndexName: "HostIndex", + }), + ); + } else { + response = await fastify.dynamoClient.send( + new ScanCommand(queryParams), + ); + } + const dynamoItems = response.Items + ? response.Items.map((x) => unmarshall(x)) + : null; + if (!dynamoItems) { + throw new NotFoundError({ + endpointName: host ? `/api/v1/ical/${host}` : "/api/v1/ical", + }); + } + // generate friendly calendar name + let calendarName = + host && host.includes("ACM") + ? `${host} Events` + : `ACM@UIUC - ${host} Events`; + if (host == "ACM") { + calendarName = "ACM@UIUC - Major Events"; + } + if (!host) { + calendarName = "ACM@UIUC - All Events"; + } + const calendar = ical({ name: calendarName }); + calendar.timezone({ + name: "America/Chicago", + generator: getVtimezoneComponent, }); + calendar.method(ICalCalendarMethod.PUBLISH); + for (const rawEvent of dynamoItems) { + let event = calendar.createEvent({ + start: moment.tz(rawEvent.start, "America/Chicago"), + end: rawEvent.end + ? moment.tz(rawEvent.end, "America/Chicago") + : moment.tz(rawEvent.start, "America/Chicago"), + summary: rawEvent.title, + description: rawEvent.locationLink + ? `Host: ${rawEvent.host}\nGoogle Maps Link: ${rawEvent.locationLink}\n\n` + + rawEvent.description + : `Host: ${rawEvent.host}\n\n` + rawEvent.description, + timezone: "America/Chicago", + organizer: generateHostName(host || "ACM"), + id: rawEvent.id, + }); - if (rawEvent.repeats) { - if (rawEvent.repeatEnds) { - event = event.repeating({ - ...repeatingIcalMap[rawEvent.repeats as EventRepeatOptions], - until: moment.tz(rawEvent.repeatEnds, "America/Chicago"), + if (rawEvent.repeats) { + if (rawEvent.repeatEnds) { + event = event.repeating({ + ...repeatingIcalMap[rawEvent.repeats as EventRepeatOptions], + until: moment.tz(rawEvent.repeatEnds, "America/Chicago"), + }); + } else { + event.repeating( + repeatingIcalMap[rawEvent.repeats as EventRepeatOptions], + ); + } + } + if (rawEvent.location) { + event = event.location({ + title: rawEvent.location, }); - } else { - event.repeating( - repeatingIcalMap[rawEvent.repeats as EventRepeatOptions], - ); } } - if (rawEvent.location) { - event = event.location({ - title: rawEvent.location, - }); - } - } - reply - .headers({ - "Content-Type": "text/calendar; charset=utf-8", - "Content-Disposition": 'attachment; filename="calendar.ics"', - }) - .send(calendar.toString()); - }); + reply + .headers({ + "Content-Type": "text/calendar; charset=utf-8", + "Content-Disposition": 'attachment; filename="calendar.ics"', + }) + .send(calendar.toString()); + }, + ); }; export default icalPlugin; diff --git a/src/api/routes/linkry.ts b/src/api/routes/linkry.ts new file mode 100644 index 00000000..ad45721f --- /dev/null +++ b/src/api/routes/linkry.ts @@ -0,0 +1,645 @@ +import { FastifyPluginAsync } from "fastify"; +import { z } from "zod"; +import { AppRoles } from "../../common/roles.js"; +import { + BaseError, + DatabaseDeleteError, + DatabaseFetchError, + DatabaseInsertError, + NotFoundError, + UnauthorizedError, + ValidationError, +} from "../../common/errors/index.js"; +import { NoDataRequest } from "../types.js"; +import { + QueryCommand, + TransactWriteItemsCommand, + TransactWriteItem, + TransactionCanceledException, +} from "@aws-sdk/client-dynamodb"; +import { CloudFrontKeyValueStoreClient } from "@aws-sdk/client-cloudfront-keyvaluestore"; +import { genericConfig } from "../../common/config.js"; +import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; +import rateLimiter from "api/plugins/rateLimiter.js"; +import { + deleteKey, + getLinkryKvArn, + setKey, +} from "api/functions/cloudfrontKvStore.js"; +import { createRequest, linkrySlug } from "common/types/linkry.js"; +import { + extractUniqueSlugs, + fetchOwnerRecords, + getGroupsForSlugs, + getFilteredUserGroups, + getDelegatedLinks, + fetchLinkEntry, + getAllLinks, +} from "api/functions/linkry.js"; +import { intersection } from "api/plugins/auth.js"; +import { createAuditLogEntry } from "api/functions/auditLog.js"; +import { Modules } from "common/modules.js"; +import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi"; +import { withRoles, withTags } from "api/components/index.js"; + +type OwnerRecord = { + slug: string; + redirect: string; + access: string; + updatedAt: string; + createdAt: string; +}; + +type AccessRecord = { + slug: string; + access: string; + createdAt: string; + updatedAt: string; +}; + +type LinkyCreateRequest = { + Params: undefined; + Querystring: undefined; + Body: z.infer; +}; + +type LinkryGetRequest = { + Params: { slug: string }; + Querystring: undefined; + Body: undefined; +}; + +const linkryRoutes: FastifyPluginAsync = async (fastify, _options) => { + const limitedRoutes: FastifyPluginAsync = async (fastify) => { + fastify.register(rateLimiter, { + limit: 30, + duration: 60, + rateLimitIdentifier: "linkry", + }); + + fastify.get( + "/redir", + { + schema: withRoles( + [AppRoles.LINKS_MANAGER, AppRoles.LINKS_ADMIN], + withTags(["Linkry"], {}), + ), + onRequest: fastify.authorizeFromSchema, + }, + async (request, reply) => { + const username = request.username!; + const tableName = genericConfig.LinkryDynamoTableName; + + // First try-catch: Fetch owner records + let ownerRecords; + try { + ownerRecords = await fetchOwnerRecords( + username, + tableName, + fastify.dynamoClient, + ); + } catch (error) { + request.log.error( + `Failed to fetch owner records: ${error instanceof Error ? error.toString() : "Unknown error"}`, + ); + throw new DatabaseFetchError({ + message: "Failed to fetch owner records from Dynamo table.", + }); + } + + const ownedUniqueSlugs = extractUniqueSlugs(ownerRecords); + + // Second try-catch: Get groups for slugs + let ownedLinksWithGroups; + try { + ownedLinksWithGroups = await getGroupsForSlugs( + ownedUniqueSlugs, + ownerRecords, + tableName, + fastify.dynamoClient, + ); + } catch (error) { + request.log.error( + `Failed to get groups for slugs: ${error instanceof Error ? error.toString() : "Unknown error"}`, + ); + throw new DatabaseFetchError({ + message: "Failed to get groups for links from Dynamo table.", + }); + } + + // Third try-catch paths: Get delegated links based on user role + let delegatedLinks; + if (request.userRoles!.has(AppRoles.LINKS_ADMIN)) { + // Admin path + try { + delegatedLinks = ( + await getAllLinks(tableName, fastify.dynamoClient) + ).filter((x) => x.owner !== username); + } catch (error) { + request.log.error( + `Failed to get all links for admin: ${error instanceof Error ? error.toString() : "Unknown error"}`, + ); + throw new DatabaseFetchError({ + message: "Failed to get all links for admin from Dynamo table.", + }); + } + } else { + // Regular user path + const userGroups = getFilteredUserGroups(request); + try { + delegatedLinks = await getDelegatedLinks( + userGroups, + ownedUniqueSlugs, + tableName, + fastify.dynamoClient, + ); + } catch (error) { + request.log.error( + `Failed to get delegated links: ${error instanceof Error ? error.toString() : "Unknown error"}`, + ); + throw new DatabaseFetchError({ + message: "Failed to get delegated links from Dynamo table.", + }); + } + } + + // Send the response + reply.code(200).send({ + ownedLinks: ownedLinksWithGroups, + delegatedLinks: delegatedLinks, + }); + }, + ); + + fastify.withTypeProvider().post( + "/redir", + { + schema: withRoles( + [AppRoles.LINKS_MANAGER, AppRoles.LINKS_ADMIN], + withTags(["Linkry"], { + body: createRequest, + }), + ), + preValidation: async (request, reply) => { + const routeAlreadyExists = fastify.hasRoute({ + url: `/${request.body.slug}`, + method: "GET", + }); + + if (routeAlreadyExists) { + throw new ValidationError({ + message: `Slug ${request.body.slug} is reserved by the system.`, + }); + } + + if (!fastify.cloudfrontKvClient) { + fastify.cloudfrontKvClient = new CloudFrontKeyValueStoreClient({ + region: genericConfig.AwsRegion, + }); + } + }, + onRequest: fastify.authorizeFromSchema, + }, + async (request, reply) => { + const { slug } = request.body; + const tableName = genericConfig.LinkryDynamoTableName; + const currentRecord = await fetchLinkEntry( + slug, + tableName, + fastify.dynamoClient, + ); + + if (currentRecord && !request.userRoles!.has(AppRoles.LINKS_ADMIN)) { + const setUserGroups = new Set(request.tokenPayload?.groups || []); + const mutualGroups = intersection( + new Set(currentRecord["access"]), + setUserGroups, + ); + if (mutualGroups.size == 0) { + throw new UnauthorizedError({ + message: + "You do not own this record and have not been delegated access.", + }); + } + } + + // Use a transaction to handle if one/multiple of these writes fail + const TransactItems: TransactWriteItem[] = []; + + try { + const mode = currentRecord ? "modify" : "create"; + request.log.info(`Operating in ${mode} mode.`); + const currentUpdatedAt = + currentRecord && currentRecord["updatedAt"] + ? currentRecord["updatedAt"] + : null; + const currentCreatedAt = + currentRecord && currentRecord["createdAt"] + ? currentRecord["createdAt"] + : null; + + // Generate new timestamp for all records + const creationTime: Date = new Date(); + const newUpdatedAt = creationTime.toISOString(); + const newCreatedAt = currentCreatedAt || newUpdatedAt; + const queryCommand = new QueryCommand({ + TableName: genericConfig.LinkryDynamoTableName, + KeyConditionExpression: + "slug = :slug AND begins_with(access, :accessPrefix)", + ExpressionAttributeValues: marshall({ + ":slug": request.body.slug, + ":accessPrefix": "GROUP#", + }), + }); + + const existingGroups = await fastify.dynamoClient.send(queryCommand); + const existingGroupSet = new Set(); + let existingGroupTimestampMismatch = false; + + if (existingGroups.Items && existingGroups.Items.length > 0) { + for (const item of existingGroups.Items) { + const unmarshalledItem = unmarshall(item); + existingGroupSet.add(unmarshalledItem.access); + + // Check if all existing GROUP records have the same updatedAt timestamp + // This ensures no other process has modified any part of the record + if ( + currentUpdatedAt && + unmarshalledItem.updatedAt && + unmarshalledItem.updatedAt !== currentUpdatedAt + ) { + existingGroupTimestampMismatch = true; + } + } + } + + // If timestamp mismatch found, reject the operation + if (existingGroupTimestampMismatch) { + throw new ValidationError({ + message: + "Record was modified by another process. Please try again.", + }); + } + + const ownerRecord: OwnerRecord = { + slug: request.body.slug, + redirect: request.body.redirect, + access: "OWNER#" + request.username, + updatedAt: newUpdatedAt, + createdAt: newCreatedAt, + }; + + // Add the OWNER record with a condition check to ensure it hasn't been modified + const ownerPutItem: TransactWriteItem = { + Put: { + TableName: genericConfig.LinkryDynamoTableName, + Item: marshall(ownerRecord), + ...(mode === "modify" + ? { + ConditionExpression: "updatedAt = :updatedAt", + ExpressionAttributeValues: marshall({ + ":updatedAt": currentUpdatedAt, + }), + } + : {}), + }, + }; + + TransactItems.push(ownerPutItem); + + // Add new GROUP records + const accessGroups: string[] = request.body.access || []; + const newGroupSet = new Set( + accessGroups.map((group) => "GROUP#" + group), + ); + + // Add new GROUP records that don't already exist + for (const accessGroup of accessGroups) { + const groupKey = "GROUP#" + accessGroup; + + // Skip if this group already exists + if (existingGroupSet.has(groupKey)) { + // Update existing GROUP record with new updatedAt + const updateItem: TransactWriteItem = { + Update: { + TableName: genericConfig.LinkryDynamoTableName, + Key: marshall({ + slug: request.body.slug, + access: groupKey, + }), + UpdateExpression: "SET updatedAt = :updatedAt", + ExpressionAttributeValues: marshall({ + ":updatedAt": newUpdatedAt, + ...(mode === "modify" + ? { ":currentUpdatedAt": currentUpdatedAt } + : {}), + }), + ...(mode === "modify" + ? { + ConditionExpression: "updatedAt = :currentUpdatedAt", + } + : {}), + }, + }; + + TransactItems.push(updateItem); + } else { + // Create new GROUP record + const groupRecord: AccessRecord = { + slug: request.body.slug, + access: groupKey, + updatedAt: newUpdatedAt, + createdAt: newCreatedAt, + }; + + const groupPutItem: TransactWriteItem = { + Put: { + TableName: genericConfig.LinkryDynamoTableName, + Item: marshall(groupRecord), + }, + }; + + TransactItems.push(groupPutItem); + } + } + + // Delete GROUP records that are no longer needed + for (const existingGroup of existingGroupSet) { + // Skip if this is a group we want to keep + if (newGroupSet.has(existingGroup)) { + continue; + } + + const deleteItem: TransactWriteItem = { + Delete: { + TableName: genericConfig.LinkryDynamoTableName, + Key: marshall({ + slug: request.body.slug, + access: existingGroup, + }), + ...(mode === "modify" + ? { + ConditionExpression: "updatedAt = :updatedAt", + ExpressionAttributeValues: marshall({ + ":updatedAt": currentUpdatedAt, + }), + } + : {}), + }, + }; + + TransactItems.push(deleteItem); + } + await fastify.dynamoClient.send( + new TransactWriteItemsCommand({ TransactItems }), + ); + } catch (e) { + fastify.log.error(e); + // Handle optimistic concurrency control + if ( + e instanceof TransactionCanceledException && + e.CancellationReasons && + e.CancellationReasons.some( + (reason) => reason.Code === "ConditionalCheckFailed", + ) + ) { + for (const reason of e.CancellationReasons) { + request.log.error(`Cancellation reason: ${reason.Message}`); + } + throw new ValidationError({ + message: + "The record was modified by another process. Please try again.", + }); + } + + if (e instanceof BaseError) { + throw e; + } + + throw new DatabaseInsertError({ + message: "Failed to save data to DynamoDB.", + }); + } + // Add to cloudfront key value store so that redirects happen at the edge + const kvArn = await getLinkryKvArn(fastify.runEnvironment); + try { + await setKey({ + key: request.body.slug, + value: request.body.redirect, + kvsClient: fastify.cloudfrontKvClient, + arn: kvArn, + }); + } catch (e) { + fastify.log.error(e); + if (e instanceof BaseError) { + throw e; + } + throw new DatabaseInsertError({ + message: "Failed to save redirect to Cloudfront KV store.", + }); + } + await createAuditLogEntry({ + dynamoClient: fastify.dynamoClient, + entry: { + module: Modules.LINKRY, + actor: request.username!, + target: request.body.slug, + message: `Created redirect to "${request.body.redirect}"`, + }, + }); + return reply.status(201).send(); + }, + ); + + fastify.get( + "/redir/:slug", + { + schema: withRoles( + [AppRoles.LINKS_MANAGER, AppRoles.LINKS_ADMIN], + withTags(["Linkry"], { + params: z.object({ + slug: linkrySlug, + }), + }), + ), + onRequest: fastify.authorizeFromSchema, + }, + async (request, reply) => { + try { + const { slug } = request.params; + const tableName = genericConfig.LinkryDynamoTableName; + // It's likely faster to just fetch and not return if not found + // Rather than checking each individual group manually + const item = await fetchLinkEntry( + slug, + tableName, + fastify.dynamoClient, + ); + if (!item) { + throw new NotFoundError({ endpointName: request.url }); + } + if (!request.userRoles!.has(AppRoles.LINKS_ADMIN)) { + const setUserGroups = new Set(request.tokenPayload?.groups || []); + const mutualGroups = intersection( + new Set(item["access"]), + setUserGroups, + ); + if (mutualGroups.size == 0) { + throw new UnauthorizedError({ + message: "You have not been delegated access.", + }); + } + } + return reply.status(200).send(item); + } catch (e: unknown) { + fastify.log.error(e); + if (e instanceof BaseError) { + throw e; + } + throw new DatabaseFetchError({ + message: "Failed to fetch slug information in Dynamo table.", + }); + } + }, + ); + + fastify.withTypeProvider().delete( + "/redir/:slug", + { + schema: withRoles( + [AppRoles.LINKS_MANAGER, AppRoles.LINKS_ADMIN], + withTags(["Linkry"], { + params: z.object({ + slug: linkrySlug, + }), + }), + ), + onRequest: async (request, reply) => { + await fastify.authorizeFromSchema(request, reply); + + if (!fastify.cloudfrontKvClient) { + fastify.cloudfrontKvClient = new CloudFrontKeyValueStoreClient({ + region: genericConfig.AwsRegion, + }); + } + }, + }, + async (request, reply) => { + const { slug } = request.params; + const tableName = genericConfig.LinkryDynamoTableName; + const currentRecord = await fetchLinkEntry( + slug, + tableName, + fastify.dynamoClient, + ); + + if (!currentRecord) { + throw new NotFoundError({ endpointName: request.url }); + } + + if (currentRecord && !request.userRoles!.has(AppRoles.LINKS_ADMIN)) { + const setUserGroups = new Set(request.tokenPayload?.groups || []); + const mutualGroups = intersection( + new Set(currentRecord["access"]), + setUserGroups, + ); + if (mutualGroups.size == 0) { + throw new UnauthorizedError({ + message: + "You do not own this record and have not been delegated access.", + }); + } + } + + const TransactItems: TransactWriteItem[] = [ + ...currentRecord.access.map((x) => ({ + Delete: { + TableName: genericConfig.LinkryDynamoTableName, + Key: { + slug: { S: slug }, + access: { S: `GROUP#${x}` }, + }, + ConditionExpression: "updatedAt = :updatedAt", + ExpressionAttributeValues: marshall({ + ":updatedAt": currentRecord.updatedAt, + }), + }, + })), + { + Delete: { + TableName: genericConfig.LinkryDynamoTableName, + Key: { + slug: { S: slug }, + access: { S: `OWNER#${currentRecord.owner}` }, + }, + ConditionExpression: "updatedAt = :updatedAt", + ExpressionAttributeValues: marshall({ + ":updatedAt": currentRecord.updatedAt, + }), + }, + }, + ]; + try { + await fastify.dynamoClient.send( + new TransactWriteItemsCommand({ TransactItems }), + ); + } catch (e) { + fastify.log.error(e); + // Handle optimistic concurrency control + if ( + e instanceof TransactionCanceledException && + e.CancellationReasons && + e.CancellationReasons.some( + (reason) => reason.Code === "ConditionalCheckFailed", + ) + ) { + for (const reason of e.CancellationReasons) { + request.log.error(`Cancellation reason: ${reason.Message}`); + } + throw new ValidationError({ + message: + "The record was modified by another process. Please try again.", + }); + } + + if (e instanceof BaseError) { + throw e; + } + + throw new DatabaseDeleteError({ + message: "Failed to delete data from DynamoDB.", + }); + } + const kvArn = await getLinkryKvArn(fastify.runEnvironment); + try { + await deleteKey({ + key: slug, + kvsClient: fastify.cloudfrontKvClient, + arn: kvArn, + }); + } catch (e) { + fastify.log.error(e); + if (e instanceof BaseError) { + throw e; + } + throw new DatabaseDeleteError({ + message: "Failed to delete redirect at Cloudfront KV store.", + }); + } + await createAuditLogEntry({ + dynamoClient: fastify.dynamoClient, + entry: { + module: Modules.LINKRY, + actor: request.username!, + target: slug, + message: `Deleted short link redirect."`, + }, + }); + reply.code(204).send(); + }, + ); + }; + fastify.register(limitedRoutes); +}; + +export default linkryRoutes; diff --git a/src/api/routes/logs.ts b/src/api/routes/logs.ts new file mode 100644 index 00000000..0a665eb2 --- /dev/null +++ b/src/api/routes/logs.ts @@ -0,0 +1,116 @@ +import { QueryCommand } from "@aws-sdk/client-dynamodb"; +import { unmarshall } from "@aws-sdk/util-dynamodb"; +import { withRoles, withTags } from "api/components/index.js"; +import { createAuditLogEntry } from "api/functions/auditLog.js"; +import rateLimiter from "api/plugins/rateLimiter.js"; +import { genericConfig } from "common/config.js"; +import { + BaseError, + DatabaseFetchError, + ValidationError, +} from "common/errors/index.js"; +import { Modules } from "common/modules.js"; +import { AppRoles } from "common/roles.js"; +import { loggingEntryFromDatabase } from "common/types/logs.js"; +import fastify, { FastifyPluginAsync } from "fastify"; +import { + FastifyZodOpenApiTypeProvider, + serializerCompiler, + validatorCompiler, +} from "fastify-zod-openapi"; +import { request } from "http"; +import { z } from "zod"; + +const responseSchema = z.array(loggingEntryFromDatabase); +type ResponseType = z.infer; + +const logsPlugin: FastifyPluginAsync = async (fastify, _options) => { + fastify.register(rateLimiter, { + limit: 10, + duration: 30, + rateLimitIdentifier: "logs", + }); + fastify.withTypeProvider().get( + "/:module", + { + schema: withRoles( + [AppRoles.AUDIT_LOG_VIEWER], + withTags(["Logging"], { + querystring: z + .object({ + start: z.coerce.number().openapi({ + description: + "Epoch timestamp for the start of the search range", + example: 1745114772, + }), + end: z.coerce.number().openapi({ + description: "Epoch timestamp for the end of the search range", + example: 1745201172, + }), + }) + .refine((data) => data.start <= data.end, { + message: "Start time must be less than or equal to end time", + path: ["start"], + }), + params: z.object({ + module: z + .nativeEnum(Modules) + .openapi({ description: "Module to get audit logs for." }), + }), + summary: "Retrieve audit logs for a module.", + // response: { 200: responseSchema }, + }), + ), + onRequest: fastify.authorizeFromSchema, + }, + async (request, reply) => { + const { module } = request.params; + const { start, end } = request.query; + const logPromise = createAuditLogEntry({ + dynamoClient: fastify.dynamoClient, + entry: { + module: Modules.AUDIT_LOG, + actor: request.username!, + target: module, + message: `Viewed audit log from ${start} to ${end}.`, + }, + }); + const queryCommand = new QueryCommand({ + TableName: genericConfig.AuditLogTable, + KeyConditionExpression: "#pk = :module AND #sk BETWEEN :start AND :end", + ExpressionAttributeNames: { + "#pk": "module", + "#sk": "createdAt", + }, + ExpressionAttributeValues: { + ":module": { S: module }, + ":start": { N: start.toString() }, + ":end": { N: end.toString() }, + }, + ScanIndexForward: false, + }); + let response; + try { + response = await fastify.dynamoClient.send(queryCommand); + if (!response.Items) { + throw new DatabaseFetchError({ + message: "Error occurred fetching audit log.", + }); + } + } catch (e) { + if (e instanceof BaseError) { + throw e; + } + fastify.log.error(e); + throw new DatabaseFetchError({ + message: "Error occurred fetching audit log.", + }); + } + await logPromise; + const resp = response.Items.map((x) => unmarshall(x)) as ResponseType; + reply.send(resp); + }, + ); +}; + +export default logsPlugin; diff --git a/src/api/routes/membership.ts b/src/api/routes/membership.ts index ab4963cc..a9546c3f 100644 --- a/src/api/routes/membership.ts +++ b/src/api/routes/membership.ts @@ -23,6 +23,13 @@ import stripe, { Stripe } from "stripe"; import { AvailableSQSFunctions, SQSPayload } from "common/types/sqsMessage.js"; import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs"; import rawbody from "fastify-raw-body"; +import { + FastifyZodOpenApiTypeProvider, + serializerCompiler, + validatorCompiler, +} from "fastify-zod-openapi"; +import { z } from "zod"; +import { withTags } from "api/components/index.js"; const NONMEMBER_CACHE_SECONDS = 60; // 1 minute const MEMBER_CACHE_SECONDS = 43200; // 12 hours @@ -69,165 +76,202 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => { duration: 30, rateLimitIdentifier: "membership", }); - fastify.get<{ - Body: undefined; - Params: { netId: string }; - }>("/checkout/:netId", async (request, reply) => { - const netId = request.params.netId.toLowerCase(); - if (!validateNetId(netId)) { - throw new ValidationError({ - message: `${netId} is not a valid Illinois NetID!`, - }); - } - if (fastify.nodeCache.get(`isMember_${netId}`) === true) { - throw new ValidationError({ - message: `${netId} is already a paid member!`, - }); - } - const isDynamoMember = await checkPaidMembershipFromTable( - netId, - fastify.dynamoClient, - ); - if (isDynamoMember) { - fastify.nodeCache.set(`isMember_${netId}`, true, MEMBER_CACHE_SECONDS); - throw new ValidationError({ - message: `${netId} is already a paid member!`, - }); - } - const entraIdToken = await getEntraIdToken( - await getAuthorizedClients(), - fastify.environmentConfig.AadValidClientId, - ); - const paidMemberGroup = fastify.environmentConfig.PaidMemberGroupId; - const isAadMember = await checkPaidMembershipFromEntra( - netId, - entraIdToken, - paidMemberGroup, - ); - if (isAadMember) { - fastify.nodeCache.set(`isMember_${netId}`, true, MEMBER_CACHE_SECONDS); - reply - .header("X-ACM-Data-Source", "aad") - .send({ netId, isPaidMember: true }); - await setPaidMembershipInTable(netId, fastify.dynamoClient); - throw new ValidationError({ - message: `${netId} is already a paid member!`, - }); - } - fastify.nodeCache.set( - `isMember_${netId}`, - false, - NONMEMBER_CACHE_SECONDS, - ); - const secretApiConfig = - (await getSecretValue( - fastify.secretsManagerClient, - genericConfig.ConfigSecretName, - )) || {}; - if (!secretApiConfig) { - throw new InternalServerError({ - message: "Could not connect to Stripe.", - }); - } - return reply.status(200).send( - await createCheckoutSession({ - successUrl: "https://acm.illinois.edu/paid", - returnUrl: "https://acm.illinois.edu/membership", - customerEmail: `${netId}@illinois.edu`, - stripeApiKey: secretApiConfig.stripe_secret_key as string, - items: [ - { price: fastify.environmentConfig.PaidMemberPriceId, quantity: 1 }, - ], - initiator: "purchase-membership", - allowPromotionCodes: true, + fastify.withTypeProvider().get( + "/checkout/:netId", + { + schema: withTags(["Membership"], { + params: z + .object({ netId: z.string().min(1) }) + .refine((data) => validateNetId(data.netId), { + message: "NetID is not valid!", + path: ["netId"], + }), + summary: + "Create a checkout session to purchase an ACM @ UIUC membership.", }), - ); - }); - fastify.get<{ - Body: undefined; - Querystring: { list?: string }; - Params: { netId: string }; - }>("/:netId", async (request, reply) => { - const netId = request.params.netId.toLowerCase(); - const list = request.query.list || "acmpaid"; - if (!validateNetId(netId)) { - throw new ValidationError({ - message: `${netId} is not a valid Illinois NetID!`, - }); - } - if (fastify.nodeCache.get(`isMember_${netId}_${list}`) !== undefined) { - return reply.header("X-ACM-Data-Source", "cache").send({ - netId, - list: list === "acmpaid" ? undefined : list, - isPaidMember: fastify.nodeCache.get(`isMember_${netId}_${list}`), - }); - } - if (list !== "acmpaid") { - const isMember = await checkExternalMembership( + }, + async (request, reply) => { + const netId = request.params.netId.toLowerCase(); + if (fastify.nodeCache.get(`isMember_${netId}`) === true) { + throw new ValidationError({ + message: `${netId} is already a paid member!`, + }); + } + const isDynamoMember = await checkPaidMembershipFromTable( netId, - list, fastify.dynamoClient, ); - fastify.nodeCache.set( - `isMember_${netId}_${list}`, - isMember, - MEMBER_CACHE_SECONDS, + if (isDynamoMember) { + fastify.nodeCache.set( + `isMember_${netId}`, + true, + MEMBER_CACHE_SECONDS, + ); + throw new ValidationError({ + message: `${netId} is already a paid member!`, + }); + } + const entraIdToken = await getEntraIdToken( + await getAuthorizedClients(), + fastify.environmentConfig.AadValidClientId, ); - return reply.header("X-ACM-Data-Source", "dynamo").send({ + const paidMemberGroup = fastify.environmentConfig.PaidMemberGroupId; + const isAadMember = await checkPaidMembershipFromEntra( netId, - list, - isPaidMember: isMember, - }); - } - const isDynamoMember = await checkPaidMembershipFromTable( - netId, - fastify.dynamoClient, - ); - if (isDynamoMember) { + entraIdToken, + paidMemberGroup, + ); + if (isAadMember) { + fastify.nodeCache.set( + `isMember_${netId}`, + true, + MEMBER_CACHE_SECONDS, + ); + reply + .header("X-ACM-Data-Source", "aad") + .send({ netId, isPaidMember: true }); + await setPaidMembershipInTable(netId, fastify.dynamoClient); + throw new ValidationError({ + message: `${netId} is already a paid member!`, + }); + } fastify.nodeCache.set( - `isMember_${netId}_${list}`, - true, - MEMBER_CACHE_SECONDS, + `isMember_${netId}`, + false, + NONMEMBER_CACHE_SECONDS, ); - return reply - .header("X-ACM-Data-Source", "dynamo") - .send({ netId, isPaidMember: true }); - } - const entraIdToken = await getEntraIdToken( - await getAuthorizedClients(), - fastify.environmentConfig.AadValidClientId, - ); - const paidMemberGroup = fastify.environmentConfig.PaidMemberGroupId; - const isAadMember = await checkPaidMembershipFromEntra( - netId, - entraIdToken, - paidMemberGroup, - ); - if (isAadMember) { + const secretApiConfig = + (await getSecretValue( + fastify.secretsManagerClient, + genericConfig.ConfigSecretName, + )) || {}; + if (!secretApiConfig) { + throw new InternalServerError({ + message: "Could not connect to Stripe.", + }); + } + return reply.status(200).send( + await createCheckoutSession({ + successUrl: "https://acm.illinois.edu/paid", + returnUrl: "https://acm.illinois.edu/membership", + customerEmail: `${netId}@illinois.edu`, + stripeApiKey: secretApiConfig.stripe_secret_key as string, + items: [ + { + price: fastify.environmentConfig.PaidMemberPriceId, + quantity: 1, + }, + ], + initiator: "purchase-membership", + allowPromotionCodes: true, + }), + ); + }, + ); + fastify.withTypeProvider().get( + "/:netId", + { + schema: withTags(["Membership"], { + params: z + .object({ netId: z.string().min(1) }) + .refine((data) => validateNetId(data.netId), { + message: "NetID is not valid!", + path: ["netId"], + }), + querystring: z.object({ + list: z.string().min(1).optional().openapi({ + description: + "Membership list to check from (defaults to ACM Paid Member list).", + }), + }), + summary: + "Check ACM @ UIUC paid membership (or partner organization membership) status.", + }), + }, + async (request, reply) => { + const netId = request.params.netId.toLowerCase(); + const list = request.query.list || "acmpaid"; + if (fastify.nodeCache.get(`isMember_${netId}_${list}`) !== undefined) { + return reply.header("X-ACM-Data-Source", "cache").send({ + netId, + list: list === "acmpaid" ? undefined : list, + isPaidMember: fastify.nodeCache.get(`isMember_${netId}_${list}`), + }); + } + if (list !== "acmpaid") { + const isMember = await checkExternalMembership( + netId, + list, + fastify.dynamoClient, + ); + fastify.nodeCache.set( + `isMember_${netId}_${list}`, + isMember, + MEMBER_CACHE_SECONDS, + ); + return reply.header("X-ACM-Data-Source", "dynamo").send({ + netId, + list, + isPaidMember: isMember, + }); + } + const isDynamoMember = await checkPaidMembershipFromTable( + netId, + fastify.dynamoClient, + ); + if (isDynamoMember) { + fastify.nodeCache.set( + `isMember_${netId}_${list}`, + true, + MEMBER_CACHE_SECONDS, + ); + return reply + .header("X-ACM-Data-Source", "dynamo") + .send({ netId, isPaidMember: true }); + } + const entraIdToken = await getEntraIdToken( + await getAuthorizedClients(), + fastify.environmentConfig.AadValidClientId, + ); + const paidMemberGroup = fastify.environmentConfig.PaidMemberGroupId; + const isAadMember = await checkPaidMembershipFromEntra( + netId, + entraIdToken, + paidMemberGroup, + ); + if (isAadMember) { + fastify.nodeCache.set( + `isMember_${netId}_${list}`, + true, + MEMBER_CACHE_SECONDS, + ); + reply + .header("X-ACM-Data-Source", "aad") + .send({ netId, isPaidMember: true }); + await setPaidMembershipInTable(netId, fastify.dynamoClient); + return; + } fastify.nodeCache.set( `isMember_${netId}_${list}`, - true, - MEMBER_CACHE_SECONDS, + false, + NONMEMBER_CACHE_SECONDS, ); - reply + return reply .header("X-ACM-Data-Source", "aad") - .send({ netId, isPaidMember: true }); - await setPaidMembershipInTable(netId, fastify.dynamoClient); - return; - } - fastify.nodeCache.set( - `isMember_${netId}_${list}`, - false, - NONMEMBER_CACHE_SECONDS, - ); - return reply - .header("X-ACM-Data-Source", "aad") - .send({ netId, isPaidMember: false }); - }); + .send({ netId, isPaidMember: false }); + }, + ); }; fastify.post( "/provision", - { config: { rawBody: true } }, + { + config: { rawBody: true }, + schema: withTags(["Membership"], { + summary: + "Stripe webhook handler to provision ACM @ UIUC membership after checkout session has completed.", + hide: true, + }), + }, async (request, reply) => { let event: Stripe.Event; if (!request.rawBody) { diff --git a/src/api/routes/mobileWallet.ts b/src/api/routes/mobileWallet.ts index fc44a5e0..65047e3c 100644 --- a/src/api/routes/mobileWallet.ts +++ b/src/api/routes/mobileWallet.ts @@ -14,6 +14,12 @@ import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs"; import { genericConfig } from "../../common/config.js"; import { zodToJsonSchema } from "zod-to-json-schema"; import rateLimiter from "api/plugins/rateLimiter.js"; +import { + FastifyZodOpenApiTypeProvider, + serializerCompiler, + validatorCompiler, +} from "fastify-zod-openapi"; +import { withTags } from "api/components/index.js"; const queuedResponseJsonSchema = zodToJsonSchema( z.object({ @@ -27,38 +33,23 @@ const mobileWalletRoute: FastifyPluginAsync = async (fastify, _options) => { duration: 30, rateLimitIdentifier: "mobileWallet", }); - fastify.post<{ Querystring: { email: string } }>( + fastify.withTypeProvider().post( "/membership", { - schema: { - response: { 202: queuedResponseJsonSchema }, - querystring: { - type: "object", - properties: { - email: { type: "string", format: "email" }, - }, - required: ["email"], - }, - }, + schema: withTags(["Mobile Wallet"], { + // response: { 202: queuedResponseJsonSchema }, + querystring: z + .object({ + email: z.string().email(), + }) + .refine((data) => data.email.endsWith("@illinois.edu"), { + message: "Email must be on the illinois.edu domain.", + path: ["email"], + }), + summary: "Email mobile wallet pass for ACM membership to user.", + }), }, async (request, reply) => { - if (!request.query.email) { - throw new UnauthenticatedError({ message: "Could not find user." }); - } - try { - await z - .string() - .email() - .refine( - (email) => email.endsWith("@illinois.edu"), - "Email must be on the illinois.edu domain.", - ) - .parseAsync(request.query.email); - } catch { - throw new ValidationError({ - message: "Email query parameter is not a valid email", - }); - } const isPaidMember = (fastify.runEnvironment === "dev" && request.query.email === "testinguser@illinois.edu") || diff --git a/src/api/routes/organizations.ts b/src/api/routes/organizations.ts index cf8c2bfb..4d86191a 100644 --- a/src/api/routes/organizations.ts +++ b/src/api/routes/organizations.ts @@ -2,6 +2,7 @@ import { FastifyPluginAsync } from "fastify"; import { OrganizationList } from "../../common/orgs.js"; import fastifyCaching from "@fastify/caching"; import rateLimiter from "api/plugins/rateLimiter.js"; +import { withTags } from "api/components/index.js"; const organizationsPlugin: FastifyPluginAsync = async (fastify, _options) => { fastify.register(fastifyCaching, { @@ -14,9 +15,17 @@ const organizationsPlugin: FastifyPluginAsync = async (fastify, _options) => { duration: 60, rateLimitIdentifier: "organizations", }); - fastify.get("/", {}, async (request, reply) => { - reply.send(OrganizationList); - }); + fastify.get( + "", + { + schema: withTags(["Generic"], { + summary: "Get a list of ACM @ UIUC sub-organizations.", + }), + }, + async (_request, reply) => { + reply.send(OrganizationList); + }, + ); }; export default organizationsPlugin; diff --git a/src/api/routes/protected.ts b/src/api/routes/protected.ts index 86a3d044..6ca72ff4 100644 --- a/src/api/routes/protected.ts +++ b/src/api/routes/protected.ts @@ -1,5 +1,6 @@ import { FastifyPluginAsync } from "fastify"; import rateLimiter from "api/plugins/rateLimiter.js"; +import { withRoles, withTags } from "api/components/index.js"; const protectedRoute: FastifyPluginAsync = async (fastify, _options) => { await fastify.register(rateLimiter, { @@ -7,10 +8,21 @@ const protectedRoute: FastifyPluginAsync = async (fastify, _options) => { duration: 30, rateLimitIdentifier: "protected", }); - fastify.get("/", async (request, reply) => { - const roles = await fastify.authorize(request, reply, []); - reply.send({ username: request.username, roles: Array.from(roles) }); - }); + fastify.get( + "", + { + schema: withRoles( + [], + withTags(["Generic"], { + summary: "Get a user's username and roles.", + }), + ), + }, + async (request, reply) => { + const roles = await fastify.authorize(request, reply, [], false); + reply.send({ username: request.username, roles: Array.from(roles) }); + }, + ); }; export default protectedRoute; diff --git a/src/api/routes/roomRequests.ts b/src/api/routes/roomRequests.ts index 90c91fb9..cb821625 100644 --- a/src/api/routes/roomRequests.ts +++ b/src/api/routes/roomRequests.ts @@ -3,7 +3,6 @@ import rateLimiter from "api/plugins/rateLimiter.js"; import { formatStatus, roomGetResponse, - roomRequestBaseSchema, RoomRequestFormValues, roomRequestPostResponse, roomRequestSchema, @@ -12,24 +11,25 @@ import { roomRequestStatusUpdateRequest, } from "common/types/roomRequest.js"; import { AppRoles } from "common/roles.js"; -import { zodToJsonSchema } from "zod-to-json-schema"; import { BaseError, DatabaseFetchError, DatabaseInsertError, InternalServerError, - UnauthenticatedError, } from "common/errors/index.js"; import { - PutItemCommand, QueryCommand, TransactWriteItemsCommand, } from "@aws-sdk/client-dynamodb"; import { genericConfig, notificationRecipients } from "common/config.js"; import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; -import { z } from "zod"; import { AvailableSQSFunctions, SQSPayload } from "common/types/sqsMessage.js"; import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs"; +import { withRoles, withTags } from "api/components/index.js"; +import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi"; +import { z } from "zod"; +import { buildAuditLogTransactPut } from "api/functions/auditLog.js"; +import { Modules } from "common/modules.js"; const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => { await fastify.register(rateLimiter, { @@ -37,22 +37,27 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => { duration: 30, rateLimitIdentifier: "roomRequests", }); - fastify.post<{ - Body: RoomRequestStatusUpdatePostBody; - Params: { requestId: string; semesterId: string }; - }>( + fastify.withTypeProvider().post( "/:semesterId/:requestId/status", { - onRequest: async (request, reply) => { - await fastify.authorize(request, reply, [AppRoles.ROOM_REQUEST_UPDATE]); - }, - preValidation: async (request, reply) => { - await fastify.zodValidateBody( - request, - reply, - roomRequestStatusUpdateRequest, - ); - }, + schema: withRoles( + [AppRoles.ROOM_REQUEST_UPDATE], + withTags(["Room Requests"], { + summary: "Create status update for a room request.", + params: z.object({ + requestId: z.string().min(1).openapi({ + description: "Room request ID.", + example: "6667e095-8b04-4877-b361-f636f459ba42", + }), + semesterId: z.string().min(1).openapi({ + description: "Short semester slug for a given semester.", + example: "sp25", + }), + }), + body: roomRequestStatusUpdateRequest, + }), + ), + onRequest: fastify.authorizeFromSchema, }, async (request, reply) => { if (!request.username) { @@ -88,7 +93,7 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => { }); } const createdAt = new Date().toISOString(); - const command = new PutItemCommand({ + const itemPut = { TableName: genericConfig.RoomRequestsStatusTableName, Item: marshall({ requestId, @@ -97,9 +102,22 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => { createdBy: request.username, ...request.body, }), + }; + const logPut = buildAuditLogTransactPut({ + entry: { + module: Modules.ROOM_RESERVATIONS, + actor: request.username!, + target: `${semesterId}/${requestId}`, + requestId: request.id, + message: `Changed status to "${formatStatus(request.body.status)}".`, + }, }); try { - await fastify.dynamoClient.send(command); + await fastify.dynamoClient.send( + new TransactWriteItemsCommand({ + TransactItems: [{ Put: itemPut }, logPut], + }), + ); } catch (e) { request.log.error(e); if (e instanceof BaseError) { @@ -144,20 +162,22 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => { return reply.status(201).send(); }, ); - fastify.get<{ - Body: undefined; - Params: { semesterId: string }; - }>( + fastify.withTypeProvider().get( "/:semesterId", { - schema: { - response: { - 200: zodToJsonSchema(roomGetResponse), - }, - }, - onRequest: async (request, reply) => { - await fastify.authorize(request, reply, [AppRoles.ROOM_REQUEST_CREATE]); - }, + schema: withRoles( + [AppRoles.ROOM_REQUEST_CREATE], + withTags(["Room Requests"], { + summary: "Get room requests for a specific semester.", + params: z.object({ + semesterId: z.string().min(1).openapi({ + description: "Short semester slug for a given semester.", + example: "sp25", + }), + }), + }), + ), + onRequest: fastify.authorizeFromSchema, }, async (request, reply) => { const semesterId = request.params.semesterId; @@ -241,18 +261,17 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => { return reply.status(200).send(itemsWithStatus); }, ); - fastify.post<{ Body: RoomRequestFormValues }>( - "/", + fastify.withTypeProvider().post( + "", { - schema: { - response: { 201: zodToJsonSchema(roomRequestPostResponse) }, - }, - preValidation: async (request, reply) => { - await fastify.zodValidateBody(request, reply, roomRequestSchema); - }, - onRequest: async (request, reply) => { - await fastify.authorize(request, reply, [AppRoles.ROOM_REQUEST_CREATE]); - }, + schema: withRoles( + [AppRoles.ROOM_REQUEST_CREATE], + withTags(["Room Requests"], { + summary: "Create a room request.", + body: roomRequestSchema, + }), + ), + onRequest: fastify.authorizeFromSchema, }, async (request, reply) => { const requestId = request.id; @@ -263,11 +282,22 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => { } const body = { ...request.body, + eventStart: request.body.eventStart.toUTCString(), + eventEnd: request.body.eventStart.toUTCString(), requestId, userId: request.username, "userId#requestId": `${request.username}#${requestId}`, semesterId: request.body.semester, }; + const logPut = buildAuditLogTransactPut({ + entry: { + module: Modules.ROOM_RESERVATIONS, + actor: request.username!, + target: `${request.body.semester}/${requestId}`, + requestId: request.id, + message: "Created room reservation request.", + }, + }); try { const createdAt = new Date().toISOString(); const transactionCommand = new TransactWriteItemsCommand({ @@ -291,6 +321,7 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => { }), }, }, + logPut, ], }); await fastify.dynamoClient.send(transactionCommand); @@ -341,15 +372,26 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => { ); }, ); - fastify.get<{ - Body: undefined; - Params: { requestId: string; semesterId: string }; - }>( + fastify.withTypeProvider().get( "/:semesterId/:requestId", { - onRequest: async (request, reply) => { - await fastify.authorize(request, reply, [AppRoles.ROOM_REQUEST_CREATE]); - }, + schema: withRoles( + [AppRoles.ROOM_REQUEST_CREATE], + withTags(["Room Requests"], { + summary: "Get specific room request data.", + params: z.object({ + requestId: z.string().min(1).openapi({ + description: "Room request ID.", + example: "6667e095-8b04-4877-b361-f636f459ba42", + }), + semesterId: z.string().min(1).openapi({ + description: "Short semester slug for a given semester.", + example: "sp25", + }), + }), + }), + ), + onRequest: fastify.authorizeFromSchema, }, async (request, reply) => { const requestId = request.params.requestId; diff --git a/src/api/routes/siglead.ts b/src/api/routes/siglead.ts index 0d2b07be..6e9e6cec 100644 --- a/src/api/routes/siglead.ts +++ b/src/api/routes/siglead.ts @@ -1,460 +1,131 @@ -import { FastifyInstance, FastifyPluginAsync } from "fastify"; -import { allAppRoles, AppRoles } from "../../common/roles.js"; -import { zodToJsonSchema } from "zod-to-json-schema"; -import { - addToTenant, - getEntraIdToken, - listGroupMembers, - modifyGroup, - patchUserProfile, -} from "../functions/entraId.js"; +import { FastifyPluginAsync } from "fastify"; +import { z } from "zod"; +import { AppRoles } from "../../common/roles.js"; import { BaseError, + DatabaseDeleteError, DatabaseFetchError, DatabaseInsertError, - EntraGroupError, - EntraInvitationError, - InternalServerError, NotFoundError, + UnauthenticatedError, UnauthorizedError, + ValidationError, } from "../../common/errors/index.js"; -import { PutItemCommand } from "@aws-sdk/client-dynamodb"; -import { genericConfig } from "../../common/config.js"; -import { marshall } from "@aws-sdk/util-dynamodb"; +import { NoDataRequest } from "../types.js"; import { - InviteUserPostRequest, - invitePostRequestSchema, - GroupMappingCreatePostRequest, - groupMappingCreatePostSchema, - entraActionResponseSchema, - groupModificationPatchSchema, - GroupModificationPatchRequest, - EntraGroupActions, - entraGroupMembershipListResponse, - ProfilePatchRequest, - entraProfilePatchRequest, -} from "../../common/types/iam.js"; + DynamoDBClient, + QueryCommand, + DeleteItemCommand, + ScanCommand, + TransactWriteItemsCommand, + AttributeValue, + TransactWriteItem, + GetItemCommand, + TransactionCanceledException, +} from "@aws-sdk/client-dynamodb"; +import { CloudFrontKeyValueStoreClient } from "@aws-sdk/client-cloudfront-keyvaluestore"; import { - AUTH_DECISION_CACHE_SECONDS, - getGroupRoles, -} from "../functions/authorization.js"; -import { OrganizationList } from "common/orgs.js"; -import { z } from "zod"; - -const OrganizationListEnum = z.enum(OrganizationList as [string, ...string[]]); -export type Org = z.infer; - -type Member = { name: string; email: string }; -type OrgMembersResponse = { org: Org; members: Member[] }; - -// const groupMappings = getRunEnvironmentConfig().KnownGroupMappings; -// const groupOptions = Object.entries(groupMappings).map(([key, value]) => ({ -// label: userGroupMappings[key as keyof KnownGroups] || key, -// value: `${key}_${value}`, // to ensure that the same group for multiple roles still renders -// })); + genericConfig, + EVENT_CACHED_DURATION, + LinkryGroupUUIDToGroupNameMap, +} from "../../common/config.js"; +import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; +import rateLimiter from "api/plugins/rateLimiter.js"; +import { + deleteKey, + getLinkryKvArn, + setKey, +} from "api/functions/cloudfrontKvStore.js"; +import { zodToJsonSchema } from "zod-to-json-schema"; +import { + SigDetailRecord, + SigleadGetRequest, + SigMemberRecord, +} from "common/types/siglead.js"; +import { fetchMemberRecords, fetchSigDetail } from "api/functions/siglead.js"; +import { intersection } from "api/plugins/auth.js"; const sigleadRoutes: FastifyPluginAsync = async (fastify, _options) => { - fastify.get<{ - Reply: OrgMembersResponse[]; - }>("/groups", async (request, reply) => { - const entraIdToken = await getEntraIdToken( + const limitedRoutes: FastifyPluginAsync = async (fastify) => { + /*fastify.register(rateLimiter, { + limit: 30, + duration: 60, + rateLimitIdentifier: "linkry", + });*/ + + fastify.get( + "/sigmembers/:sigid", { - smClient: fastify.secretsManagerClient, - dynamoClient: fastify.dynamoClient, + onRequest: async (request, reply) => { + /*await fastify.authorize(request, reply, [ + AppRoles.LINKS_MANAGER, + AppRoles.LINKS_ADMIN, + ]);*/ + }, }, - fastify.environmentConfig.AadValidClientId, - ); + async (request, reply) => { + const { sigid } = request.params; + const tableName = genericConfig.SigleadDynamoSigMemberTableName; - const data = await Promise.all( - OrganizationList.map(async (org) => { - const members: Member[] = await listGroupMembers(entraIdToken, org); - return { org, members } as OrgMembersResponse; - }), - ); + // First try-catch: Fetch owner records + let memberRecords: SigMemberRecord[]; + try { + memberRecords = await fetchMemberRecords( + sigid, + tableName, + fastify.dynamoClient, + ); + } catch (error) { + request.log.error( + `Failed to fetch member records: ${error instanceof Error ? error.toString() : "Unknown error"}`, + ); + throw new DatabaseFetchError({ + message: "Failed to fetch member records from Dynamo table.", + }); + } - reply.status(200).send(data); - }); + // Send the response + reply.code(200).send(memberRecords); + }, + ); - // fastify.patch<{ Body: ProfilePatchRequest }>( - // "/profile", - // { - // preValidation: async (request, reply) => { - // await fastify.zodValidateBody(request, reply, entraProfilePatchRequest); - // }, - // onRequest: async (request, reply) => { - // await fastify.authorize(request, reply, allAppRoles); - // }, - // }, - // async (request, reply) => { - // if (!request.tokenPayload || !request.username) { - // throw new UnauthorizedError({ - // message: "User does not have the privileges for this task.", - // }); - // } - // const userOid = request.tokenPayload["oid"]; - // const entraIdToken = await getEntraIdToken( - // { - // smClient: fastify.secretsManagerClient, - // dynamoClient: fastify.dynamoClient, - // }, - // fastify.environmentConfig.AadValidClientId, - // ); - // await patchUserProfile( - // entraIdToken, - // request.username, - // userOid, - // request.body, - // ); - // reply.send(201); - // }, - // ); - // fastify.get<{ - // Body: undefined; - // Querystring: { groupId: string }; - // }>( - // "/groups/:groupId/roles", - // { - // schema: { - // querystring: { - // type: "object", - // properties: { - // groupId: { - // type: "string", - // }, - // }, - // }, - // }, - // onRequest: async (request, reply) => { - // await fastify.authorize(request, reply, [AppRoles.IAM_ADMIN]); - // }, - // }, - // async (request, reply) => { - // try { - // const groupId = (request.params as Record).groupId; - // const roles = await getGroupRoles( - // fastify.dynamoClient, - // fastify, - // groupId, - // ); - // return reply.send(roles); - // } catch (e: unknown) { - // if (e instanceof BaseError) { - // throw e; - // } + fastify.get( + "/sigdetail/:sigid", + { + onRequest: async (request, reply) => { + /*await fastify.authorize(request, reply, [ + AppRoles.LINKS_MANAGER, + AppRoles.LINKS_ADMIN, + ]);*/ + }, + }, + async (request, reply) => { + const { sigid } = request.params; + const tableName = genericConfig.SigleadDynamoSigDetailTableName; - // request.log.error(e); - // throw new DatabaseFetchError({ - // message: "An error occurred finding the group role mapping.", - // }); - // } - // }, - // ); - // fastify.post<{ - // Body: GroupMappingCreatePostRequest; - // Querystring: { groupId: string }; - // }>( - // "/groups/:groupId/roles", - // { - // schema: { - // querystring: { - // type: "object", - // properties: { - // groupId: { - // type: "string", - // }, - // }, - // }, - // }, - // preValidation: async (request, reply) => { - // await fastify.zodValidateBody( - // request, - // reply, - // groupMappingCreatePostSchema, - // ); - // }, - // onRequest: async (request, reply) => { - // await fastify.authorize(request, reply, [AppRoles.IAM_ADMIN]); - // }, - // }, - // async (request, reply) => { - // const groupId = (request.params as Record).groupId; - // try { - // const timestamp = new Date().toISOString(); - // const command = new PutItemCommand({ - // TableName: `${genericConfig.IAMTablePrefix}-grouproles`, - // Item: marshall({ - // groupUuid: groupId, - // roles: request.body.roles, - // createdAt: timestamp, - // }), - // }); - // await fastify.dynamoClient.send(command); - // fastify.nodeCache.set( - // `grouproles-${groupId}`, - // request.body.roles, - // AUTH_DECISION_CACHE_SECONDS, - // ); - // } catch (e: unknown) { - // fastify.nodeCache.del(`grouproles-${groupId}`); - // if (e instanceof BaseError) { - // throw e; - // } + // First try-catch: Fetch owner records + let sigDetail: SigDetailRecord; + try { + sigDetail = await fetchSigDetail( + sigid, + tableName, + fastify.dynamoClient, + ); + } catch (error) { + request.log.error( + `Failed to fetch sig detail record: ${error instanceof Error ? error.toString() : "Unknown error"}`, + ); + throw new DatabaseFetchError({ + message: "Failed to fetch sig detail record from Dynamo table.", + }); + } - // request.log.error(e); - // throw new DatabaseInsertError({ - // message: "Could not create group role mapping.", - // }); - // } - // reply.send({ message: "OK" }); - // request.log.info( - // { type: "audit", actor: request.username, target: groupId }, - // `set target roles to ${request.body.roles.toString()}`, - // ); - // }, - // ); - // fastify.post<{ Body: InviteUserPostRequest }>( - // "/inviteUsers", - // { - // schema: { - // response: { 202: zodToJsonSchema(entraActionResponseSchema) }, - // }, - // preValidation: async (request, reply) => { - // await fastify.zodValidateBody(request, reply, invitePostRequestSchema); - // }, - // onRequest: async (request, reply) => { - // await fastify.authorize(request, reply, [AppRoles.IAM_INVITE_ONLY]); - // }, - // }, - // async (request, reply) => { - // const emails = request.body.emails; - // const entraIdToken = await getEntraIdToken( - // { - // smClient: fastify.secretsManagerClient, - // dynamoClient: fastify.dynamoClient, - // }, - // fastify.environmentConfig.AadValidClientId, - // ); - // if (!entraIdToken) { - // throw new InternalServerError({ - // message: "Could not get Entra ID token to perform task.", - // }); - // } - // const response: Record[]> = { - // success: [], - // failure: [], - // }; - // const results = await Promise.allSettled( - // emails.map((email) => addToTenant(entraIdToken, email)), - // ); - // for (let i = 0; i < results.length; i++) { - // const result = results[i]; - // if (result.status === "fulfilled") { - // request.log.info( - // { type: "audit", actor: request.username, target: emails[i] }, - // "invited user to Entra ID tenant.", - // ); - // response.success.push({ email: emails[i] }); - // } else { - // request.log.info( - // { type: "audit", actor: request.username, target: emails[i] }, - // "failed to invite user to Entra ID tenant.", - // ); - // if (result.reason instanceof EntraInvitationError) { - // response.failure.push({ - // email: emails[i], - // message: result.reason.message, - // }); - // } else { - // response.failure.push({ - // email: emails[i], - // message: "An unknown error occurred.", - // }); - // } - // } - // } - // reply.status(202).send(response); - // }, - // ); - // fastify.patch<{ - // Body: GroupModificationPatchRequest; - // Querystring: { groupId: string }; - // }>( - // "/groups/:groupId", - // { - // schema: { - // querystring: { - // type: "object", - // properties: { - // groupId: { - // type: "string", - // }, - // }, - // }, - // }, - // preValidation: async (request, reply) => { - // await fastify.zodValidateBody( - // request, - // reply, - // groupModificationPatchSchema, - // ); - // }, - // onRequest: async (request, reply) => { - // await fastify.authorize(request, reply, [AppRoles.IAM_ADMIN]); - // }, - // }, - // async (request, reply) => { - // const groupId = (request.params as Record).groupId; - // if (!groupId || groupId === "") { - // throw new NotFoundError({ - // endpointName: request.url, - // }); - // } - // if (genericConfig.ProtectedEntraIDGroups.includes(groupId)) { - // throw new EntraGroupError({ - // code: 403, - // message: - // "This group is protected and cannot be modified by this service. You must log into Entra ID directly to modify this group.", - // group: groupId, - // }); - // } - // const entraIdToken = await getEntraIdToken( - // { - // smClient: fastify.secretsManagerClient, - // dynamoClient: fastify.dynamoClient, - // }, - // fastify.environmentConfig.AadValidClientId, - // ); - // const addResults = await Promise.allSettled( - // request.body.add.map((email) => - // modifyGroup(entraIdToken, email, groupId, EntraGroupActions.ADD), - // ), - // ); - // const removeResults = await Promise.allSettled( - // request.body.remove.map((email) => - // modifyGroup(entraIdToken, email, groupId, EntraGroupActions.REMOVE), - // ), - // ); - // const response: Record[]> = { - // success: [], - // failure: [], - // }; - // for (let i = 0; i < addResults.length; i++) { - // const result = addResults[i]; - // if (result.status === "fulfilled") { - // response.success.push({ email: request.body.add[i] }); - // request.log.info( - // { - // type: "audit", - // actor: request.username, - // target: request.body.add[i], - // }, - // `added target to group ID ${groupId}`, - // ); - // } else { - // request.log.info( - // { - // type: "audit", - // actor: request.username, - // target: request.body.add[i], - // }, - // `failed to add target to group ID ${groupId}`, - // ); - // if (result.reason instanceof EntraGroupError) { - // response.failure.push({ - // email: request.body.add[i], - // message: result.reason.message, - // }); - // } else { - // response.failure.push({ - // email: request.body.add[i], - // message: "An unknown error occurred.", - // }); - // } - // } - // } - // for (let i = 0; i < removeResults.length; i++) { - // const result = removeResults[i]; - // if (result.status === "fulfilled") { - // response.success.push({ email: request.body.remove[i] }); - // request.log.info( - // { - // type: "audit", - // actor: request.username, - // target: request.body.remove[i], - // }, - // `removed target from group ID ${groupId}`, - // ); - // } else { - // request.log.info( - // { - // type: "audit", - // actor: request.username, - // target: request.body.add[i], - // }, - // `failed to remove target from group ID ${groupId}`, - // ); - // if (result.reason instanceof EntraGroupError) { - // response.failure.push({ - // email: request.body.add[i], - // message: result.reason.message, - // }); - // } else { - // response.failure.push({ - // email: request.body.add[i], - // message: "An unknown error occurred.", - // }); - // } - // } - // } - // reply.status(202).send(response); - // }, - // ); - // fastify.get<{ - // Querystring: { groupId: string }; - // }>( - // "/groups/:groupId", - // { - // schema: { - // response: { 200: zodToJsonSchema(entraGroupMembershipListResponse) }, - // querystring: { - // type: "object", - // properties: { - // groupId: { - // type: "string", - // }, - // }, - // }, - // }, - // onRequest: async (request, reply) => { - // await fastify.authorize(request, reply, [AppRoles.IAM_ADMIN]); - // }, - // }, - // async (request, reply) => { - // const groupId = (request.params as Record).groupId; - // if (!groupId || groupId === "") { - // throw new NotFoundError({ - // endpointName: request.url, - // }); - // } - // if (genericConfig.ProtectedEntraIDGroups.includes(groupId)) { - // throw new EntraGroupError({ - // code: 403, - // message: - // "This group is protected and cannot be read by this service. You must log into Entra ID directly to read this group.", - // group: groupId, - // }); - // } - // const entraIdToken = await getEntraIdToken( - // { - // smClient: fastify.secretsManagerClient, - // dynamoClient: fastify.dynamoClient, - // }, - // fastify.environmentConfig.AadValidClientId, - // ); - // const response = await listGroupMembers(entraIdToken, groupId); - // reply.status(200).send(response); - // }, - // ); + // Send the response + reply.code(200).send(sigDetail); + }, + ); + }; + fastify.register(limitedRoutes); }; export default sigleadRoutes; diff --git a/src/api/routes/stripe.ts b/src/api/routes/stripe.ts index 12cbb2a3..acb56490 100644 --- a/src/api/routes/stripe.ts +++ b/src/api/routes/stripe.ts @@ -1,11 +1,17 @@ import { - PutItemCommand, QueryCommand, ScanCommand, + TransactWriteItemsCommand, } from "@aws-sdk/client-dynamodb"; import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; +import { withRoles, withTags } from "api/components/index.js"; +import { + buildAuditLogTransactPut, + createAuditLogEntry, +} from "api/functions/auditLog.js"; import { createStripeLink, + deactivateStripeLink, StripeLinkCreateParams, } from "api/functions/stripe.js"; import { getSecretValue } from "api/plugins/auth.js"; @@ -13,9 +19,11 @@ import { genericConfig } from "common/config.js"; import { BaseError, DatabaseFetchError, + DatabaseInsertError, InternalServerError, UnauthenticatedError, } from "common/errors/index.js"; +import { Modules } from "common/modules.js"; import { AppRoles } from "common/roles.js"; import { invoiceLinkPostResponseSchema, @@ -23,19 +31,19 @@ import { invoiceLinkGetResponseSchema, } from "common/types/stripe.js"; import { FastifyPluginAsync } from "fastify"; -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; +import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi"; const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => { - fastify.get( + fastify.withTypeProvider().get( "/paymentLinks", { - schema: { - response: { 200: zodToJsonSchema(invoiceLinkGetResponseSchema) }, - }, - onRequest: async (request, reply) => { - await fastify.authorize(request, reply, [AppRoles.STRIPE_LINK_CREATOR]); - }, + schema: withRoles( + [AppRoles.STRIPE_LINK_CREATOR], + withTags(["Stripe"], { + summary: "Get available Stripe payment links.", + }), + ), + onRequest: fastify.authorizeFromSchema, }, async (request, reply) => { let dynamoCommand; @@ -82,22 +90,17 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => { reply.status(200).send(parsed); }, ); - fastify.post<{ Body: z.infer }>( + fastify.withTypeProvider().post( "/paymentLinks", { - schema: { - response: { 201: zodToJsonSchema(invoiceLinkPostResponseSchema) }, - }, - preValidation: async (request, reply) => { - await fastify.zodValidateBody( - request, - reply, - invoiceLinkPostRequestSchema, - ); - }, - onRequest: async (request, reply) => { - await fastify.authorize(request, reply, [AppRoles.STRIPE_LINK_CREATOR]); - }, + schema: withRoles( + [AppRoles.STRIPE_LINK_CREATOR], + withTags(["Stripe"], { + summary: "Create a Stripe payment link.", + body: invoiceLinkPostRequestSchema, + }), + ), + onRequest: fastify.authorizeFromSchema, }, async (request, reply) => { if (!request.username) { @@ -121,30 +124,53 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => { const { url, linkId, priceId, productId } = await createStripeLink(payload); const invoiceId = request.body.invoiceId; - const dynamoCommand = new PutItemCommand({ - TableName: genericConfig.StripeLinksDynamoTableName, - Item: marshall({ - userId: request.username, - linkId, - priceId, - productId, - invoiceId, - url, - amount: request.body.invoiceAmountUsd, - active: true, - createdAt: new Date().toISOString(), - }), - }); - await fastify.dynamoClient.send(dynamoCommand); - request.log.info( - { - type: "audit", - module: "stripe", + const logStatement = buildAuditLogTransactPut({ + entry: { + module: Modules.STRIPE, actor: request.username, target: `Link ${linkId} | Invoice ${invoiceId}`, + message: "Created Stripe payment link", }, - "Created Stripe payment link", - ); + }); + const dynamoCommand = new TransactWriteItemsCommand({ + TransactItems: [ + logStatement, + { + Put: { + TableName: genericConfig.StripeLinksDynamoTableName, + Item: marshall({ + userId: request.username, + linkId, + priceId, + productId, + invoiceId, + url, + amount: request.body.invoiceAmountUsd, + active: true, + createdAt: new Date().toISOString(), + }), + }, + }, + ], + }); + try { + await fastify.dynamoClient.send(dynamoCommand); + } catch (e) { + await deactivateStripeLink({ + stripeApiKey: secretApiConfig.stripe_secret_key as string, + linkId, + }); + fastify.log.info( + `Deactivated Stripe link ${linkId} due to error in writing to database.`, + ); + if (e instanceof BaseError) { + throw e; + } + fastify.log.error(e); + throw new DatabaseInsertError({ + message: "Could not write Stripe link to database.", + }); + } reply.status(201).send({ id: linkId, link: url }); }, ); diff --git a/src/api/routes/tickets.ts b/src/api/routes/tickets.ts index 0ad8dd0d..6129eaa3 100644 --- a/src/api/routes/tickets.ts +++ b/src/api/routes/tickets.ts @@ -10,6 +10,7 @@ import { genericConfig } from "../../common/config.js"; import { BaseError, DatabaseFetchError, + DatabaseInsertError, NotFoundError, NotSupportedError, TicketNotFoundError, @@ -17,10 +18,15 @@ import { UnauthenticatedError, ValidationError, } from "../../common/errors/index.js"; -import { unmarshall } from "@aws-sdk/util-dynamodb"; +import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; import { validateEmail } from "../functions/validation.js"; import { AppRoles } from "../../common/roles.js"; import { zodToJsonSchema } from "zod-to-json-schema"; +import { ItemPostData, postMetadataSchema } from "common/types/tickets.js"; +import { createAuditLogEntry } from "api/functions/auditLog.js"; +import { Modules } from "common/modules.js"; +import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi"; +import { withRoles, withTags } from "api/components/index.js"; const postMerchSchema = z.object({ type: z.literal("merch"), @@ -99,32 +105,27 @@ type TicketsGetRequest = { Body: undefined; }; -type TicketsListRequest = { - Params: undefined; - Querystring: undefined; - Body: undefined; -}; - const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => { - fastify.get( - "/", + fastify.withTypeProvider().get( + "", { - schema: { - response: { - 200: listMerchItemsResponseJsonSchema, - }, - }, - onRequest: async (request, reply) => { - await fastify.authorize(request, reply, [ - AppRoles.TICKETS_MANAGER, - AppRoles.TICKETS_SCANNER, - ]); - }, + schema: withRoles( + [AppRoles.TICKETS_MANAGER, AppRoles.TICKETS_SCANNER], + withTags(["Tickets/Merchandise"], { + summary: "Retrieve metadata about tickets/merchandise items.", + }), + ), + onRequest: fastify.authorizeFromSchema, }, async (request, reply) => { let isTicketingManager = true; try { - await fastify.authorize(request, reply, [AppRoles.TICKETS_MANAGER]); + await fastify.authorize( + request, + reply, + [AppRoles.TICKETS_MANAGER], + false, + ); } catch { isTicketingManager = false; } @@ -200,34 +201,30 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => { }); } } - reply.send({ merch: merchItems, tickets: ticketItems }); }, ); - fastify.get( + fastify.withTypeProvider().get( "/:eventId", { - schema: { - querystring: { - type: "object", - properties: { - type: { - type: "string", - enum: ["merch", "ticket"], - }, - }, - }, - response: { - 200: getTicketsResponseJsonSchema, - }, - }, - onRequest: async (request, reply) => { - await fastify.authorize(request, reply, [AppRoles.TICKETS_MANAGER]); - }, + schema: withRoles( + [AppRoles.TICKETS_MANAGER], + withTags(["Tickets/Merchandise"], { + summary: "Get detailed per-sale information by event ID.", + querystring: z.object({ + type: z.enum(["merch", "ticket"]), + }), + params: z.object({ + eventId: z.string().min(1), + }), + security: [], + }), + ), + onRequest: fastify.authorizeFromSchema, }, async (request, reply) => { - const eventId = (request.params as Record).eventId; - const eventType = request.query?.type; + const eventId = request.params.eventId; + const eventType = request.query.type; const issuedTickets: TicketInfoEntry[] = []; switch (eventType) { case "merch": @@ -271,18 +268,90 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => { return reply.send(response); }, ); - fastify.post<{ Body: VerifyPostRequest }>( + fastify.withTypeProvider().patch( + "/:eventId", + { + schema: withRoles( + [AppRoles.TICKETS_MANAGER], + withTags(["Tickets/Merchandise"], { + summary: "Modify event metadata.", + params: z.object({ + eventId: z.string().min(1), + }), + body: postMetadataSchema, + }), + ), + onRequest: fastify.authorizeFromSchema, + }, + async (request, reply) => { + const eventId = request.params.eventId; + const eventType = request.body.type; + const eventActiveSet = request.body.itemSalesActive; + let newActiveTime: number = 0; + if (typeof eventActiveSet === "boolean") { + if (!eventActiveSet) { + newActiveTime = -1; + } + } else { + newActiveTime = parseInt( + (eventActiveSet.valueOf() / 1000).toFixed(0), + 10, + ); + } + let command: UpdateItemCommand; + switch (eventType) { + case "merch": + command = new UpdateItemCommand({ + TableName: genericConfig.MerchStoreMetadataTableName, + Key: marshall({ item_id: eventId }), + UpdateExpression: "SET item_sales_active_utc = :new_val", + ConditionExpression: "item_id = :item_id", + ExpressionAttributeValues: { + ":new_val": { N: newActiveTime.toString() }, + ":item_id": { S: eventId }, + }, + }); + break; + case "ticket": + command = new UpdateItemCommand({ + TableName: genericConfig.TicketMetadataTableName, + Key: marshall({ event_id: eventId }), + UpdateExpression: "SET event_sales_active_utc = :new_val", + ConditionExpression: "event_id = :item_id", + ExpressionAttributeValues: { + ":new_val": { N: newActiveTime.toString() }, + ":item_id": { S: eventId }, + }, + }); + break; + } + try { + await fastify.dynamoClient.send(command); + } catch (e) { + if (e instanceof ConditionalCheckFailedException) { + throw new NotFoundError({ + endpointName: request.url, + }); + } + fastify.log.error(e); + throw new DatabaseInsertError({ + message: "Could not update active time for item.", + }); + } + return reply.status(201).send(); + }, + ); + fastify.withTypeProvider().post( "/checkIn", { - schema: { - response: { 200: responseJsonSchema }, - }, - preValidation: async (request, reply) => { - await fastify.zodValidateBody(request, reply, postSchema); - }, - onRequest: async (request, reply) => { - await fastify.authorize(request, reply, [AppRoles.TICKETS_SCANNER]); - }, + schema: withRoles( + [AppRoles.TICKETS_SCANNER], + withTags(["Tickets/Merchandise"], { + summary: "Mark a ticket/merch item as fulfilled by QR code data.", + body: postSchema, + }), + ), + onRequest: fastify.authorizeFromSchema, }, async (request, reply) => { let command: UpdateItemCommand; @@ -402,15 +471,16 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => { ticketId, purchaserData, }); - request.log.info( - { - type: "audit", - module: "tickets", - actor: request.username, + await createAuditLogEntry({ + dynamoClient: fastify.dynamoClient, + entry: { + module: Modules.TICKETS, + actor: request.username!, target: ticketId, + message: `checked in ticket of type "${request.body.type}" ${request.body.type === "merch" ? `purchased by email ${request.body.email}.` : "."}`, + requestId: request.id, }, - `checked in ticket of type "${request.body.type}" ${request.body.type === "merch" ? `purchased by email ${request.body.email}.` : "."}`, - ); + }); }, ); }; diff --git a/src/api/routes/vending.ts b/src/api/routes/vending.ts index 0b65136d..da2764d2 100644 --- a/src/api/routes/vending.ts +++ b/src/api/routes/vending.ts @@ -1,4 +1,6 @@ +import { withTags } from "api/components/index.js"; import { FastifyPluginAsync } from "fastify"; +import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi"; import { z } from "zod"; const postSchema = z.object({ @@ -7,36 +9,40 @@ const postSchema = z.object({ price: z.number().min(0), }); -type VendingItemPostRequest = z.infer; - const vendingPlugin: FastifyPluginAsync = async (fastify, _options) => { - fastify.get("/items", async (request, reply) => { - reply.send({ - items: [ - { - slots: ["A1"], - id: "ronitpic", - name: "A Picture of Ronit", - image_url: "https://static.acm.illinois.edu/ronit.jpeg", - price: 999, - calories: null, - fat: null, - carbs: null, - fiber: null, - sugar: null, - protein: null, - quantity: 100, - locations: null, - }, - ], - }); - }); - fastify.post<{ Body: VendingItemPostRequest }>( + fastify.get( + "/items", + { + schema: withTags(["Vending"], {}), + }, + async (request, reply) => { + reply.send({ + items: [ + { + slots: ["A1"], + id: "ronitpic", + name: "A Picture of Ronit", + image_url: "https://static.acm.illinois.edu/ronit.jpeg", + price: 999, + calories: null, + fat: null, + carbs: null, + fiber: null, + sugar: null, + protein: null, + quantity: 100, + locations: null, + }, + ], + }); + }, + ); + fastify.withTypeProvider().post( "/items", { - preValidation: async (request, reply) => { - await fastify.zodValidateBody(request, reply, postSchema); - }, + schema: withTags(["Vending"], { + body: postSchema, + }), }, async (request, reply) => { reply.send({ status: "Not implemented." }); diff --git a/src/api/sqs/emailNotifications.ts b/src/api/sqs/emailNotifications.ts index 1b9bddd9..d130cca4 100644 --- a/src/api/sqs/emailNotifications.ts +++ b/src/api/sqs/emailNotifications.ts @@ -2,6 +2,8 @@ import { AvailableSQSFunctions } from "common/types/sqsMessage.js"; import { currentEnvironmentConfig, SQSHandlerFunction } from "./index.js"; import { SendEmailCommand, SESClient } from "@aws-sdk/client-ses"; import { genericConfig } from "common/config.js"; +import { createAuditLogEntry } from "api/functions/auditLog.js"; +import { Modules } from "common/modules.js"; const stripHtml = (html: string): string => { return html @@ -41,18 +43,17 @@ export const emailNotificationsHandler: SQSHandlerFunction< }, }, }); + const logPromise = createAuditLogEntry({ + entry: { + module: Modules.EMAIL_NOTIFICATION, + actor: metadata.initiator, + target: to.join(";"), + message: `Sent email notification with subject "${subject}".`, + }, + }); const sesClient = new SESClient({ region: genericConfig.AwsRegion }); const response = await sesClient.send(command); logger.info("Sent!"); - logger.info( - { - type: "audit", - module: "emailNotification", - actor: metadata.initiator, - reqId: metadata.reqId, - target: to, - }, - `Sent email notification with subject "${subject}".`, - ); + await logPromise; return response; }; diff --git a/src/api/sqs/handlers.ts b/src/api/sqs/handlers.ts index 4cd4b492..c0034f17 100644 --- a/src/api/sqs/handlers.ts +++ b/src/api/sqs/handlers.ts @@ -21,6 +21,8 @@ import { SESClient } from "@aws-sdk/client-ses"; import pino from "pino"; import { getRoleCredentials } from "api/functions/sts.js"; import { setPaidMembership } from "api/functions/membership.js"; +import { createAuditLogEntry } from "api/functions/auditLog.js"; +import { Modules } from "common/modules.js"; const getAuthorizedClients = async ( logger: pino.Logger, @@ -108,20 +110,19 @@ export const provisionNewMemberHandler: SQSHandlerFunction< paidMemberGroup: currentEnvironmentConfig.PaidMemberGroupId, }); if (updated) { - logger.info( - { - type: "audit", - module: "provisionNewMember", + const logPromise = createAuditLogEntry({ + entry: { + module: Modules.PROVISION_NEW_MEMBER, actor: metadata.initiator, target: email, + message: "Marked target as a paid member.", }, - "marked user as a paid member.", - ); + }); logger.info( `${email} added as a paid member. Emailing their membership pass.`, ); - await emailMembershipPassHandler(payload, metadata, logger); + await logPromise; } else { logger.info(`${email} was already a paid member.`); } diff --git a/src/api/types.d.ts b/src/api/types.d.ts index 26eee1bd..34ab418c 100644 --- a/src/api/types.d.ts +++ b/src/api/types.d.ts @@ -7,6 +7,8 @@ import NodeCache from "node-cache"; import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; import { SQSClient } from "@aws-sdk/client-sqs"; +import { CloudFrontKeyValueStoreClient } from "@aws-sdk/client-cloudfront-keyvaluestore"; +import { AvailableAuthorizationPolicy } from "common/policies/definition.js"; declare module "fastify" { interface FastifyInstance { @@ -18,11 +20,11 @@ declare module "fastify" { request: FastifyRequest, reply: FastifyReply, validRoles: AppRoles[], + disableApiKeyAuth: boolean, ) => Promise>; - zodValidateBody: ( + authorizeFromSchema: ( request: FastifyRequest, - _reply: FastifyReply, - zodSchema: Zod.ZodTypeAny, + reply: FastifyReply, ) => Promise; runEnvironment: RunEnvironment; environmentConfig: ConfigType; @@ -30,11 +32,19 @@ declare module "fastify" { dynamoClient: DynamoDBClient; sqsClient?: SQSClient; secretsManagerClient: SecretsManagerClient; + cloudfrontKvClient: CloudFrontKeyValueStoreClient; } interface FastifyRequest { startTime: number; username?: string; userRoles?: Set; tokenPayload?: AadToken; + policyRestrictions?: AvailableAuthorizationPolicy[]; } } + +export type NoDataRequest = { + Params: undefined; + Querystring: undefined; + Body: undefined; +}; diff --git a/src/api/zod-openapi-patch.js b/src/api/zod-openapi-patch.js new file mode 100644 index 00000000..7317a4d4 --- /dev/null +++ b/src/api/zod-openapi-patch.js @@ -0,0 +1 @@ +import "zod-openapi/extend"; diff --git a/src/common/config.ts b/src/common/config.ts index 86420366..8208da45 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -13,6 +13,7 @@ export type ConfigType = { AzureRoleMapping: AzureRoleMapping; ValidCorsOrigins: ValueOrArray | OriginFunction; AadValidClientId: string; + LinkryBaseUrl: string PasskitIdentifier: string; PasskitSerialNumber: string; MembershipApiEndpoint: string; @@ -20,12 +21,17 @@ export type ConfigType = { SqsQueueUrl: string; PaidMemberGroupId: string; PaidMemberPriceId: string; + AadValidReadOnlyClientId: string; + LinkryCloudfrontKvArn?: string; }; export type GenericConfigType = { RateLimiterDynamoTableName: string; EventsDynamoTableName: string; CacheDynamoTableName: string; + LinkryDynamoTableName: string; + SigleadDynamoSigDetailTableName: string; + SigleadDynamoSigMemberTableName: string; StripeLinksDynamoTableName: string; ConfigSecretName: string; EntraSecretName: string; @@ -42,6 +48,9 @@ export type GenericConfigType = { ProtectedEntraIDGroups: string[]; // these groups are too privileged to be modified via this portal and must be modified directly in Entra ID. RoomRequestsTableName: string; RoomRequestsStatusTableName: string; + EntraReadOnlySecretName: string; + AuditLogTable: string; + ApiKeyTable: string; }; type EnvironmentConfigType = { @@ -49,21 +58,26 @@ type EnvironmentConfigType = { }; export const infraChairsGroupId = "c0702752-50da-49da-83d4-bcbe6f7a9b1b"; -export const officersGroupId = "ff49e948-4587-416b-8224-65147540d5fc"; +export const officersGroupId = "c4ddcc9f-a9c0-47e7-98c1-f1b345d53121"; export const officersGroupTestingId = "0e6e9199-506f-4ede-9d1b-e73f6811c9e5"; export const execCouncilGroupId = "ad81254b-4eeb-4c96-8191-3acdce9194b1"; export const execCouncilTestingGroupId = "dbe18eb2-9675-46c4-b1ef-749a6db4fedd"; export const commChairsTestingGroupId = "d714adb7-07bb-4d4d-a40a-b035bc2a35a3"; export const commChairsGroupId = "105e7d32-7289-435e-a67a-552c7f215507"; export const miscTestingGroupId = "ff25ec56-6a33-420d-bdb0-51d8a3920e46"; +export const orgsGroupId = "0b3be7c2-748e-46ce-97e7-cf86f9ca7337"; const genericConfig: GenericConfigType = { RateLimiterDynamoTableName: "infra-core-api-rate-limiter", EventsDynamoTableName: "infra-core-api-events", StripeLinksDynamoTableName: "infra-core-api-stripe-links", CacheDynamoTableName: "infra-core-api-cache", + LinkryDynamoTableName: "infra-core-api-linkry", + SigleadDynamoSigDetailTableName: "infra-core-api-sig-details", + SigleadDynamoSigMemberTableName: "infra-core-api-sig-member-details", ConfigSecretName: "infra-core-api-config", EntraSecretName: "infra-core-api-entra", + EntraReadOnlySecretName: "infra-core-api-ro-entra", UpcomingEventThresholdSeconds: 1800, // 30 mins AwsRegion: process.env.AWS_REGION || "us-east-1", EntraTenantId: "c8d9148f-9a59-4db3-827d-42ea0c2b6e2e", @@ -76,7 +90,9 @@ const genericConfig: GenericConfigType = { MembershipTableName: "infra-core-api-membership-provisioning", ExternalMembershipTableName: "infra-core-api-membership-external", RoomRequestsTableName: "infra-core-api-room-requests", - RoomRequestsStatusTableName: "infra-core-api-room-requests-status" + RoomRequestsStatusTableName: "infra-core-api-room-requests-status", + AuditLogTable: "infra-core-api-audit-log", + ApiKeyTable: "infra-core-api-keys", } as const; const environmentConfig: EnvironmentConfigType = { @@ -90,6 +106,7 @@ const environmentConfig: EnvironmentConfigType = { /http:\/\/localhost:\d+$/, ], AadValidClientId: "39c28870-94e4-47ee-b4fb-affe0bf96c9f", + LinkryBaseUrl: "https://core.aws.qa.acmuiuc.org/", PasskitIdentifier: "pass.org.acmuiuc.qa.membership", PasskitSerialNumber: "0", MembershipApiEndpoint: @@ -99,6 +116,8 @@ const environmentConfig: EnvironmentConfigType = { "https://sqs.us-east-1.amazonaws.com/427040638965/infra-core-api-sqs", PaidMemberGroupId: "9222451f-b354-4e64-ba28-c0f367a277c2", PaidMemberPriceId: "price_1R4TcTDGHrJxx3mKI6XF9cNG", + AadValidReadOnlyClientId: "2c6a0057-5acc-496c-a4e5-4adbf88387ba", + LinkryCloudfrontKvArn: "arn:aws:cloudfront::427040638965:key-value-store/0c2c02fd-7c47-4029-975d-bc5d0376bba1" }, prod: { UserFacingUrl: "https://core.acm.illinois.edu/", @@ -110,6 +129,7 @@ const environmentConfig: EnvironmentConfigType = { /http:\/\/localhost:\d+$/, ], AadValidClientId: "5e08cf0f-53bb-4e09-9df2-e9bdc3467296", + LinkryBaseUrl: "https://go.acm.illinois.edu/", PasskitIdentifier: "pass.edu.illinois.acm.membership", PasskitSerialNumber: "0", MembershipApiEndpoint: @@ -119,6 +139,7 @@ const environmentConfig: EnvironmentConfigType = { "https://sqs.us-east-1.amazonaws.com/298118738376/infra-core-api-sqs", PaidMemberGroupId: "172fd9ee-69f0-4384-9786-41ff1a43cf8e", PaidMemberPriceId: "price_1MUGIRDiGOXU9RuSChPYK6wZ", + AadValidReadOnlyClientId: "2c6a0057-5acc-496c-a4e5-4adbf88387ba" }, }; @@ -159,4 +180,13 @@ const notificationRecipients: NotificationRecipientsType = { } } +export const LinkryGroupUUIDToGroupNameMap = new Map([ + ['ad81254b-4eeb-4c96-8191-3acdce9194b1', 'ACM Exec'], + ['270c2d58-11f6-4c45-a217-d46a035fe853', 'ACM Link Shortener Managers'], + ['c4ddcc9f-a9c0-47e7-98c1-f1b345d53121', 'ACM Officers'], + ['f8dfc4cf-456b-4da3-9053-f7fdeda5d5d6', 'ACM Infra Leads'], + ['c0702752-50da-49da-83d4-bcbe6f7a9b1b', 'ACM Infra Chairs'], + ['940e4f9e-6891-4e28-9e29-148798495cdb', 'ACM Infra Team'] +]); + export { genericConfig, environmentConfig, roleArns, notificationRecipients }; diff --git a/src/common/errors/index.ts b/src/common/errors/index.ts index 2204f2b6..a2f7f58e 100644 --- a/src/common/errors/index.ts +++ b/src/common/errors/index.ts @@ -180,6 +180,17 @@ export class NotSupportedError extends BaseError<"NotSupportedError"> { } } +export class DatabaseDeleteError extends BaseError<"DatabaseDeleteError"> { + constructor({ message }: { message: string }) { + super({ + name: "DatabaseDeleteError", + id: 111, + message, + httpStatusCode: 500, + }); + } +} + export class EntraGroupError extends BaseError<"EntraGroupError"> { group: string; constructor({ @@ -202,6 +213,27 @@ export class EntraGroupError extends BaseError<"EntraGroupError"> { } } +export class EntraGroupsFromEmailError extends BaseError<"EntraGroupsFromEmailError"> { + email: string; + constructor({ + code, + message, + email + }: { + code?: number; + message?: string; + email: string + }) { + super({ + name: "EntraGroupsFromEmailError", + id: 309, //TODO: What should this be? + message: message || `Could not fetch the groups for user ${email}.`, + httpStatusCode: code || 500 + }); + this.email = email; + } + }; + export class EntraFetchError extends BaseError<"EntraFetchError"> { email: string; constructor({ message, email }: { message?: string; email: string }) { diff --git a/src/common/modules.ts b/src/common/modules.ts new file mode 100644 index 00000000..edac32ed --- /dev/null +++ b/src/common/modules.ts @@ -0,0 +1,28 @@ +export enum Modules { + IAM = "iam", + EVENTS = "events", + STRIPE = "stripe", + TICKETS = "tickets", + EMAIL_NOTIFICATION = "emailNotification", + PROVISION_NEW_MEMBER = "provisionNewMember", + MOBILE_WALLET = "mobileWallet", + LINKRY = "linkry", + AUDIT_LOG = "auditLog", + API_KEY = "apiKey", + ROOM_RESERVATIONS = "roomReservations", +} + + +export const ModulesToHumanName: Record = { + [Modules.IAM]: "IAM", + [Modules.EVENTS]: "Events", + [Modules.STRIPE]: "Stripe Integration", + [Modules.TICKETS]: "Ticketing/Merch", + [Modules.EMAIL_NOTIFICATION]: "Email Notifications", + [Modules.PROVISION_NEW_MEMBER]: "Member Provisioning", + [Modules.MOBILE_WALLET]: "Mobile Wallet", + [Modules.LINKRY]: "Link Shortener", + [Modules.AUDIT_LOG]: "Audit Log", + [Modules.API_KEY]: "API Keys", + [Modules.ROOM_RESERVATIONS]: "Room Reservations", +} diff --git a/src/common/policies/definition.ts b/src/common/policies/definition.ts new file mode 100644 index 00000000..6f662534 --- /dev/null +++ b/src/common/policies/definition.ts @@ -0,0 +1,42 @@ +import { FastifyRequest } from "fastify"; +import { hostRestrictionPolicy } from "./events.js"; +import { z } from "zod"; +import { AuthorizationPolicyResult } from "./evaluator.js"; +type Policy> = { + name: string; + paramsSchema: TParamsSchema; + evaluator: ( + request: FastifyRequest, + params: z.infer, + ) => AuthorizationPolicyResult; +}; + +// Type to get parameters type from a policy +type PolicyParams = T extends Policy ? z.infer : never; + +// Type for a registry of policies +type PolicyRegistry = { + [key: string]: Policy; +}; + +// Type to generate a strongly-typed version of the policy registry +type TypedPolicyRegistry = { + [K in keyof T]: { + name: T[K]["name"]; + params: PolicyParams; + }; +}; + +export type AvailableAuthorizationPolicies = TypedPolicyRegistry< + typeof AuthorizationPoliciesRegistry +>; +export const AuthorizationPoliciesRegistry = { + EventsHostRestrictionPolicy: hostRestrictionPolicy, +} as const; + +export type AvailableAuthorizationPolicy = { + [K in keyof typeof AuthorizationPoliciesRegistry]: { + name: K; + params: PolicyParams<(typeof AuthorizationPoliciesRegistry)[K]>; + }; +}[keyof typeof AuthorizationPoliciesRegistry]; diff --git a/src/common/policies/evaluator.ts b/src/common/policies/evaluator.ts new file mode 100644 index 00000000..e1356ccf --- /dev/null +++ b/src/common/policies/evaluator.ts @@ -0,0 +1,71 @@ +import { z } from "zod"; +import { FastifyRequest } from "fastify"; + +export const AuthorizationPolicyResultSchema = z.object({ + allowed: z.boolean(), + message: z.string(), + cacheKey: z.string().nullable(), +}); +export type AuthorizationPolicyResult = z.infer< + typeof AuthorizationPolicyResultSchema +>; + +export function createPolicy>( + name: string, + paramsSchema: TParamsSchema, + evaluatorFn: ( + request: FastifyRequest, + params: z.infer, + ) => AuthorizationPolicyResult, +) { + return { + name, + paramsSchema, + evaluator: evaluatorFn, + }; +} + +export function applyPolicy>( + policy: { + name: string; + paramsSchema: TParamsSchema; + evaluator: ( + request: FastifyRequest, + params: z.infer, + ) => AuthorizationPolicyResult; + }, + params: Record, +) { + // Validate and transform parameters using the schema + const validatedParams = policy.paramsSchema.parse(params); + + return { + policy, + params: validatedParams, + }; +} + +export function evaluatePolicy>( + request: FastifyRequest, + policyConfig: { + policy: { + name: string; + paramsSchema: TParamsSchema; + evaluator: ( + request: FastifyRequest, + params: z.infer, + ) => AuthorizationPolicyResult; + }; + params: z.infer; + }, +): AuthorizationPolicyResult { + try { + return policyConfig.policy.evaluator(request, policyConfig.params); + } catch (error: any) { + return { + cacheKey: `error:${policyConfig.policy.name}:${error.message}`, + allowed: false, + message: `Error evaluating policy ${policyConfig.policy.name}: ${error.message}`, + }; + } +} diff --git a/src/common/policies/events.ts b/src/common/policies/events.ts new file mode 100644 index 00000000..9894f63a --- /dev/null +++ b/src/common/policies/events.ts @@ -0,0 +1,38 @@ +import { z } from "zod"; +import { createPolicy } from "./evaluator.js"; +import { OrganizationList } from "../orgs.js"; +import { FastifyRequest } from "fastify"; + +export const hostRestrictionPolicy = createPolicy( + "EventsHostRestrictionPolicy", + z.object({ host: z.array(z.enum(OrganizationList)) }), + (request: FastifyRequest, params) => { + if (!request.url.startsWith("/api/v1/events")) { + return { + allowed: true, + message: "Skipped as route not in scope.", + cacheKey: null, + }; + } + const typedBody = request.body as { host: string }; + if (!typedBody || !typedBody["host"]) { + return { + allowed: true, + message: "Skipped as no host found.", + cacheKey: null, + }; + } + if (!params.host.includes(typedBody["host"])) { + return { + allowed: false, + message: `Denied by policy "EventsHostRestrictionPolicy".`, + cacheKey: request.username || null, + }; + } + return { + allowed: true, + message: `Policy "EventsHostRestrictionPolicy". evaluated successfully.`, + cacheKey: request.username || null, + }; + }, +); diff --git a/src/common/roles.ts b/src/common/roles.ts index 284d143e..9fe55393 100644 --- a/src/common/roles.ts +++ b/src/common/roles.ts @@ -10,8 +10,12 @@ export enum AppRoles { IAM_INVITE_ONLY = "invite:iam", STRIPE_LINK_CREATOR = "create:stripeLink", BYPASS_OBJECT_LEVEL_AUTH = "bypass:ola", + LINKS_MANAGER = "manage:links", + LINKS_ADMIN = "admin:links", ROOM_REQUEST_CREATE = "create:roomRequest", - ROOM_REQUEST_UPDATE = "update:roomRequest" + ROOM_REQUEST_UPDATE = "update:roomRequest", + AUDIT_LOG_VIEWER = "view:auditLog", + MANAGE_ORG_API_KEYS = "manage:orgApiKey" } export const allAppRoles = Object.values(AppRoles).filter( (value) => typeof value === "string", diff --git a/src/common/types/apiKey.ts b/src/common/types/apiKey.ts new file mode 100644 index 00000000..d31030b8 --- /dev/null +++ b/src/common/types/apiKey.ts @@ -0,0 +1,73 @@ +import { AuthorizationPoliciesRegistry, AvailableAuthorizationPolicy } from "../policies/definition.js"; +import { AppRoles } from "../roles.js"; +import { z } from "zod"; +import { InternalServerError } from "../errors/index.js"; +export type ApiKeyMaskedEntry = { + keyId: string; + roles: AppRoles[]; + owner: string; + description: string; + createdAt: number; + expiresAt?: number; + restrictions?: AvailableAuthorizationPolicy[]; +} +export type ApiKeyDynamoEntry = ApiKeyMaskedEntry & { + keyHash: string; +}; + +export type DecomposedApiKey = { + prefix: string; + id: string; + rawKey: string; + checksum: string; +}; + +const policySchemas = Object.entries(AuthorizationPoliciesRegistry).map( + ([key, policy]) => + z.object({ + name: z.literal(key), + params: policy.paramsSchema, + }) +); + +if (policySchemas.length === 0) { + throw new InternalServerError({ + message: "No authorization policies are defined in AuthorizationPoliciesRegistry. 'restrictions' will be an empty schema." + }) +} + +const policyUnion = policySchemas.length > 0 + ? z.discriminatedUnion("name", policySchemas as [typeof policySchemas[0], ...typeof policySchemas]) + : z.never(); + +export const apiKeyAllowedRoles = [ + AppRoles.EVENTS_MANAGER, + AppRoles.TICKETS_MANAGER, + AppRoles.TICKETS_SCANNER, + AppRoles.ROOM_REQUEST_CREATE, + AppRoles.STRIPE_LINK_CREATOR, + AppRoles.LINKS_MANAGER, +]; + +export const apiKeyPostBody = z.object({ + roles: z.array(z.enum(apiKeyAllowedRoles as [AppRoles, ...AppRoles[]])) + .min(1) + .refine((items) => new Set(items).size === items.length, { + message: "All roles must be unique, no duplicate values allowed", + }).openapi({ + description: `Roles granted to the API key. These roles are a subset of the overall application roles.`, + }), + description: z.string().min(1).openapi({ + description: "Description of the key's use.", + example: "Publish events to ACM Calendar as part of the CI process.", + }), + expiresAt: z.optional(z.number().refine((val) => val === undefined || val > Date.now() / 1000, { + message: "expiresAt must be a future epoch time.", + })).openapi({ + description: "Epoch timestamp of when the key expires.", + example: 1745362658, + }), + restrictions: z.optional(z.array(policyUnion)).openapi({ description: "Policy restrictions applied to the API key." }), +}); + +export type ApiKeyPostBody = z.infer; diff --git a/src/common/types/events.ts b/src/common/types/events.ts new file mode 100644 index 00000000..bd23ae40 --- /dev/null +++ b/src/common/types/events.ts @@ -0,0 +1,38 @@ +import { z } from "zod"; + +export const MAX_METADATA_KEYS = 10; +export const MAX_KEY_LENGTH = 50; +export const MAX_VALUE_LENGTH = 1000; + +export const metadataSchema = z + .record(z.string()) + .optional() + .superRefine((metadata, ctx) => { + if (!metadata) return; + + const keys = Object.keys(metadata); + + if (keys.length > MAX_METADATA_KEYS) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Metadata may have at most ${MAX_METADATA_KEYS} keys.`, + }); + } + + for (const key of keys) { + if (key.length > MAX_KEY_LENGTH) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Metadata key "${key}" exceeds ${MAX_KEY_LENGTH} characters.`, + }); + } + + const value = metadata[key]; + if (value.length > MAX_VALUE_LENGTH) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Metadata value for key "${key}" exceeds ${MAX_VALUE_LENGTH} characters.`, + }); + } + } + }); diff --git a/src/common/types/linkry.ts b/src/common/types/linkry.ts new file mode 100644 index 00000000..1a532e69 --- /dev/null +++ b/src/common/types/linkry.ts @@ -0,0 +1,47 @@ +import { z } from "zod"; + +export type ShortLinkEntry = { + slug: string; + access: string; + redir?: string; +} + +export const LINKRY_MAX_SLUG_LENGTH = 1000; + +export const getRequest = z.object({ + slug: z.string().min(1).max(LINKRY_MAX_SLUG_LENGTH).optional(), +}); + +export const linkrySlug = z.string().min(1).max(LINKRY_MAX_SLUG_LENGTH).openapi({ description: "Linkry shortened URL path.", example: "shortened_url" }) +export const linkryAccessList = z.array(z.string()).openapi({ + description: "List of groups to which access has been delegated.", example: ["c6a21a09-97c1-4f10-8ddd-fca11f967dc3", "88019d41-6c0b-4783-925c-3eb861a1ca0d"] +}) + + +export const createRequest = z.object({ + slug: linkrySlug, + access: linkryAccessList, + redirect: z.string().url().min(1).openapi({ description: "Full URL to redirect to when the short URL is visited.", example: "https://google.com/" }), +}); + +export const linkRecord = z.object({ + access: linkryAccessList, + slug: linkrySlug, + createdAt: z.string().datetime(), + updatedAt: z.string().datetime(), + redirect: z.string().url(), + owner: z.string() +}) + +export const delegatedLinkRecord = linkRecord.extend({ + owner: z.string().min(1) +}) + +export type LinkRecord = z.infer; + +export type DelegatedLinkRecord = z.infer; + +export const getLinksResponse = z.object({ + ownedLinks: z.array(linkRecord), + delegatedLinks: z.array(linkRecord) +}) diff --git a/src/common/types/logs.ts b/src/common/types/logs.ts new file mode 100644 index 00000000..77fd1666 --- /dev/null +++ b/src/common/types/logs.ts @@ -0,0 +1,17 @@ +import { Modules } from "../modules.js"; +import { z } from "zod"; + +export const loggingEntry = z.object({ + module: z.nativeEnum(Modules), + actor: z.string().min(1), + target: z.string().min(1), + requestId: z.optional(z.string().min(1).uuid()), + message: z.string().min(1) +}) + +export const loggingEntryFromDatabase = loggingEntry.extend({ + createdAt: z.number().min(1), + expireAt: z.number().min(2) +}) + +export type AuditLogEntry = z.infer diff --git a/src/common/types/roomRequest.ts b/src/common/types/roomRequest.ts index 8e711797..0ab530e8 100644 --- a/src/common/types/roomRequest.ts +++ b/src/common/types/roomRequest.ts @@ -184,10 +184,10 @@ export const roomRequestSchema = roomRequestBaseSchema // Existing fields hostingMinors: z.boolean(), locationType: z.enum(["in-person", "virtual", "both"]), - spaceType: z.string().min(1), - specificRoom: z.string().min(1), - estimatedAttendees: z.number().positive(), - seatsNeeded: z.number().positive(), + spaceType: z.optional(z.string().min(1)), + specificRoom: z.optional(z.string().min(1)), + estimatedAttendees: z.optional(z.number().positive()), + seatsNeeded: z.optional(z.number().positive()), setupDetails: z.string().min(1).nullable().optional(), onCampusPartners: z.string().min(1).nullable(), offCampusPartners: z.string().min(1).nullable(), @@ -266,10 +266,10 @@ export const roomRequestSchema = roomRequestBaseSchema ) .refine( (data) => { - if (data.setupDetails === undefined && specificRoomSetupRooms.includes(data.spaceType)) { + if (data.setupDetails === undefined && specificRoomSetupRooms.includes(data.spaceType || "")) { return false; } - if (data.setupDetails && !specificRoomSetupRooms.includes(data.spaceType)) { + if (data.setupDetails && !specificRoomSetupRooms.includes(data.spaceType || "")) { return false; } return true; @@ -280,9 +280,11 @@ export const roomRequestSchema = roomRequestBaseSchema }, ) .superRefine((data, ctx) => { - // Additional validation for conditional fields based on locationType - if (data.locationType === "in-person" || data.locationType === "both") { - if (!data.spaceType || data.spaceType.length === 0) { + const isPhysicalLocation = data.locationType === "in-person" || data.locationType === "both"; + + // Conditional physical location fields + if (isPhysicalLocation) { + if (!data.spaceType || data.spaceType.trim().length === 0) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Please select a space type", @@ -290,7 +292,7 @@ export const roomRequestSchema = roomRequestBaseSchema }); } - if (!data.specificRoom || data.specificRoom.length === 0) { + if (!data.specificRoom || data.specificRoom.trim().length === 0) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Please provide details about the room location", @@ -298,7 +300,7 @@ export const roomRequestSchema = roomRequestBaseSchema }); } - if (!data.estimatedAttendees || data.estimatedAttendees <= 0) { + if (data.estimatedAttendees == null || data.estimatedAttendees <= 0) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Please provide an estimated number of attendees", @@ -306,7 +308,7 @@ export const roomRequestSchema = roomRequestBaseSchema }); } - if (!data.seatsNeeded || data.seatsNeeded <= 0) { + if (data.seatsNeeded == null || data.seatsNeeded <= 0) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Please specify how many seats you need", @@ -357,8 +359,26 @@ export const roomRequestSchema = roomRequestBaseSchema path: ["nonIllinoisAttendees"], }); } + + // Setup details logic + if (data.setupDetails === undefined && specificRoomSetupRooms.includes(data.spaceType || "")) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Invalid setup details response.", + path: ["setupDetails"], + }); + } + + if (data.setupDetails && !specificRoomSetupRooms.includes(data.spaceType || "")) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Invalid setup details response.", + path: ["setupDetails"], + }); + } }); + export type RoomRequestFormValues = z.infer; export const roomRequestGetResponse = z.object({ diff --git a/src/common/types/siglead.ts b/src/common/types/siglead.ts new file mode 100644 index 00000000..48e3a2d9 --- /dev/null +++ b/src/common/types/siglead.ts @@ -0,0 +1,18 @@ +export type SigDetailRecord = { + sigid: string; + signame: string; + description: string; + }; + + export type SigMemberRecord = { + sigGroupId: string; + email: string; + designation: string; + memberName: string; + }; + + export type SigleadGetRequest = { + Params: { sigid: string }; + Querystring: undefined; + Body: undefined; + }; \ No newline at end of file diff --git a/src/common/types/tickets.ts b/src/common/types/tickets.ts new file mode 100644 index 00000000..4a2431bc --- /dev/null +++ b/src/common/types/tickets.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; +export const postMetadataSchema = z.object({ + type: z.union([z.literal("merch"), z.literal("ticket")]), + itemSalesActive: z.union([z.date(), z.boolean()]), +}) + +export type ItemPostData = z.infer; diff --git a/src/ui/Router.tsx b/src/ui/Router.tsx index 6c1e7c6f..99f0af8b 100644 --- a/src/ui/Router.tsx +++ b/src/ui/Router.tsx @@ -2,7 +2,6 @@ import { Anchor } from '@mantine/core'; import { element } from 'prop-types'; import React, { useState, useEffect, ReactNode } from 'react'; import { createBrowserRouter, Navigate, RouterProvider, useLocation } from 'react-router-dom'; - import { AcmAppShell } from './components/AppShell'; import { useAuth } from './components/AuthContext'; import AuthCallback from './components/AuthContext/AuthCallbackHandler.page'; @@ -13,6 +12,8 @@ import { LoginPage } from './pages/Login.page'; import { LogoutPage } from './pages/Logout.page'; import { ManageEventPage } from './pages/events/ManageEvent.page'; import { ViewEventsPage } from './pages/events/ViewEvents.page'; +import { LinkShortener } from './pages/linkry/LinkShortener.page'; +import { ManageLinkPage } from './pages/linkry/ManageLink.page'; import { ScanTicketsPage } from './pages/tickets/ScanTickets.page'; import { SelectTicketsPage } from './pages/tickets/SelectEventId.page'; import { ViewTicketsPage } from './pages/tickets/ViewTickets.page'; @@ -20,8 +21,12 @@ import { ManageIamPage } from './pages/iam/ManageIam.page'; import { ManageProfilePage } from './pages/profile/ManageProfile.page'; import { ManageStripeLinksPage } from './pages/stripe/ViewLinks.page'; import { ManageSigLeadsPage } from './pages/siglead/ManageSigLeads.page'; +import { ViewSigLeadPage } from './pages/siglead/ViewSigLead.page'; import { ManageRoomRequestsPage } from './pages/roomRequest/RoomRequestLanding.page'; import { ViewRoomRequest } from './pages/roomRequest/ViewRoomRequest.page'; +import { ViewLogsPage } from './pages/logs/ViewLogs.page'; +import { TermsOfService } from './pages/tos/TermsOfService.page'; +import { ManageApiKeysPage } from './pages/apiKeys/ManageKeys.page'; const ProfileRediect: React.FC = () => { const location = useLocation(); @@ -81,6 +86,10 @@ const commonRoutes = [ path: '/auth/callback', element: , }, + { + path: '/tos', + element: , + }, ]; const profileRouter = createBrowserRouter([ @@ -145,6 +154,18 @@ const authenticatedRouter = createBrowserRouter([ path: '/events/manage', element: , }, + { + path: '/linkry', + element: , + }, + { + path: '/linkry/add', + element: , + }, + { + path: '/linkry/edit/:slug', + element: , + }, { path: '/tickets/scan', element: , @@ -169,6 +190,10 @@ const authenticatedRouter = createBrowserRouter([ path: '/siglead-management', element: , }, + { + path: '/siglead-management/:sigId', + element: , + }, { path: '/roomRequests', element: , @@ -177,6 +202,14 @@ const authenticatedRouter = createBrowserRouter([ path: '/roomRequests/:semesterId/:requestId', element: , }, + { + path: '/logs', + element: , + }, + { + path: '/apiKeys', + element: , + }, // Catch-all route for authenticated users shows 404 page { path: '*', diff --git a/src/ui/components/AppShell/index.tsx b/src/ui/components/AppShell/index.tsx index 2cfc8bbe..cb1cb3e0 100644 --- a/src/ui/components/AppShell/index.tsx +++ b/src/ui/components/AppShell/index.tsx @@ -1,4 +1,5 @@ import { + Anchor, AppShell, Divider, Group, @@ -19,6 +20,8 @@ import { IconLock, IconDoor, IconUsers, + IconHistory, + IconKey, } from '@tabler/icons-react'; import { ReactNode } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -74,15 +77,30 @@ export const navItems = [ description: null, validRoles: [AppRoles.ROOM_REQUEST_CREATE, AppRoles.ROOM_REQUEST_UPDATE], }, -]; - -export const extLinks = [ { - link: 'https://go.acm.illinois.edu/create', + link: '/linkry', name: 'Link Shortener', icon: IconLink, description: null, + validRoles: [AppRoles.LINKS_MANAGER, AppRoles.LINKS_ADMIN], }, + { + link: '/logs', + name: 'Audit Logs', + icon: IconHistory, + description: null, + validRoles: [AppRoles.AUDIT_LOG_VIEWER], + }, + { + link: '/apiKeys', + name: 'API Keys', + icon: IconKey, + description: null, + validRoles: [AppRoles.MANAGE_ORG_API_KEYS], + }, +]; + +export const extLinks = [ { link: 'https://go.acm.illinois.edu/reimburse', name: 'Funding and Reimbursement Requests', @@ -190,6 +208,7 @@ const AcmAppShell: React.FC = ({ } const [opened, { toggle }] = useDisclosure(); const { userData } = useAuth(); + const navigate = useNavigate(); return ( = ({ Revision {getCurrentRevision()} + navigate('/tos')}> + Terms of Service + )} diff --git a/src/ui/components/AuthContext/index.tsx b/src/ui/components/AuthContext/index.tsx index 7f132079..415e7c8f 100644 --- a/src/ui/components/AuthContext/index.tsx +++ b/src/ui/components/AuthContext/index.tsx @@ -47,11 +47,7 @@ interface AuthProviderProps { } export const clearAuthCache = () => { - for (const key of Object.keys(sessionStorage)) { - if (key.startsWith(CACHE_KEY_PREFIX)) { - sessionStorage.removeItem(key); - } - } + sessionStorage.clear(); }; export const AuthProvider: React.FC = ({ children }) => { diff --git a/src/ui/components/BlurredTextDisplay.tsx b/src/ui/components/BlurredTextDisplay.tsx new file mode 100644 index 00000000..157c00c2 --- /dev/null +++ b/src/ui/components/BlurredTextDisplay.tsx @@ -0,0 +1,58 @@ +import React, { useState } from 'react'; +import { Text, Overlay, Box, ActionIcon } from '@mantine/core'; +import { IconEye, IconEyeOff } from '@tabler/icons-react'; + +export const BlurredTextDisplay: React.FC<{ text: string; initialState?: boolean }> = ({ + text, + initialState = false, +}) => { + const [visible, setVisible] = useState(initialState); + + return ( + + + {text} + + + {!visible && ( + + )} + + setVisible((v) => !v)} + pos="absolute" + top={5} + right={5} + style={{ zIndex: 10 }} + > + {visible ? : } + + + ); +}; diff --git a/src/ui/main.tsx b/src/ui/main.tsx index b3c1a514..c4308787 100644 --- a/src/ui/main.tsx +++ b/src/ui/main.tsx @@ -1,3 +1,4 @@ +import 'zod-openapi/extend'; import { Configuration, PublicClientApplication } from '@azure/msal-browser'; import { MsalProvider } from '@azure/msal-react'; import ReactDOM from 'react-dom/client'; diff --git a/src/ui/package.json b/src/ui/package.json index 3a63ed13..444023d6 100644 --- a/src/ui/package.json +++ b/src/ui/package.json @@ -28,7 +28,7 @@ "@mantine/notifications": "^7.12.0", "@tabler/icons-react": "^3.29.0", "@ungap/with-resolvers": "^0.1.0", - "axios": "^1.7.3", + "axios": "^1.8.4", "dayjs": "^1.11.12", "dotenv": "^16.4.5", "dotenv-cli": "^8.0.0", @@ -42,7 +42,8 @@ "react-pdftotext": "^1.3.0", "react-qr-reader": "^3.0.0-beta-1", "react-router-dom": "^6.26.0", - "zod": "^3.23.8" + "zod": "^3.23.8", + "zod-openapi": "^4.2.4" }, "devDependencies": { "@eslint/compat": "^1.1.1", diff --git a/src/ui/pages/Error404.page.tsx b/src/ui/pages/Error404.page.tsx index 3f970f00..0fac9b73 100644 --- a/src/ui/pages/Error404.page.tsx +++ b/src/ui/pages/Error404.page.tsx @@ -1,24 +1,16 @@ import { Container, Title, Text, Anchor } from '@mantine/core'; import React from 'react'; +import { AcmAppShell } from '@ui/components/AppShell'; -import { HeaderNavbar } from '@ui/components/Navbar'; - -export const Error404Page: React.FC<{ showNavbar?: boolean }> = ({ showNavbar }) => { - const realStuff = ( - <> - Page Not Found - - Perhaps you would like to go home? - - - ); - if (!showNavbar) { - return realStuff; - } +export const Error404Page: React.FC = () => { return ( - <> - - {realStuff} - + + + Page Not Found + + Perhaps you would like to go home? + + + ); }; diff --git a/src/ui/pages/apiKeys/ManageKeys.page.tsx b/src/ui/pages/apiKeys/ManageKeys.page.tsx new file mode 100644 index 00000000..d3848a6d --- /dev/null +++ b/src/ui/pages/apiKeys/ManageKeys.page.tsx @@ -0,0 +1,34 @@ +import React, { useState } from 'react'; +import { Card, Container, Divider, Title, Text } from '@mantine/core'; +import { AuthGuard } from '@ui/components/AuthGuard'; +import { AppRoles } from '@common/roles'; +import { useApi } from '@ui/util/api'; +import { OrgApiKeyTable } from './ManageKeysTable'; + +export const ManageApiKeysPage: React.FC = () => { + const api = useApi('core'); + + return ( + + + API Keys + Manage organization API keys. + + These keys' permissions are not tied to any one user, and can be managed by organization + admins. + + + api.get('/api/v1/apiKey/org').then((res) => res.data)} + deleteApiKeys={(ids) => + Promise.all(ids.map((id) => api.delete(`/api/v1/apiKey/org/${id}`))).then(() => {}) + } + createApiKey={(data) => api.post('/api/v1/apiKey/org', data).then((res) => res.data)} + /> + + + ); +}; diff --git a/src/ui/pages/apiKeys/ManageKeysTable.test.tsx b/src/ui/pages/apiKeys/ManageKeysTable.test.tsx new file mode 100644 index 00000000..d25c6c13 --- /dev/null +++ b/src/ui/pages/apiKeys/ManageKeysTable.test.tsx @@ -0,0 +1,213 @@ +import React from 'react'; +import { render, screen, act, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { vi } from 'vitest'; +import { MantineProvider } from '@mantine/core'; +import { notifications } from '@mantine/notifications'; +import { OrgApiKeyTable } from './ManageKeysTable'; +import { MemoryRouter } from 'react-router-dom'; +import { ApiKeyMaskedEntry, ApiKeyPostBody } from '@common/types/apiKey'; +import { AppRoles } from '@common/roles'; + +// Mock the notifications module +vi.mock('@mantine/notifications', () => ({ + notifications: { + show: vi.fn(), + }, +})); + +// Mock the AuthContext +vi.mock('@ui/components/AuthContext', async () => { + return { + useAuth: vi.fn().mockReturnValue({ + userData: { email: 'test@example.com' }, + }), + }; +}); + +// Mock BlurredTextDisplay component +vi.mock('../../components/BlurredTextDisplay', () => ({ + BlurredTextDisplay: ({ text }: { text: string }) =>
{text}
, +})); + +// Mock Modal component to avoid portal issues in tests +vi.mock('@mantine/core', async () => { + const actual = await vi.importActual('@mantine/core'); + return { + ...actual, + Modal: ({ children, opened, onClose, title }: any) => + opened ? ( +
+

{title}

+
{children}
+ +
+ ) : null, + }; +}); + +describe('OrgApiKeyTable Tests', () => { + const getApiKeys = vi.fn(); + const deleteApiKeys = vi.fn(); + const createApiKey = vi.fn(); + + const mockApiKeys: ApiKeyMaskedEntry[] = [ + { + keyId: 'key123', + description: 'Test API Key 1', + owner: 'test@example.com', + createdAt: Math.floor(Date.now() / 1000) - 86400, // yesterday + expiresAt: Math.floor(Date.now() / 1000) + 86400 * 30, // 30 days from now + roles: [AppRoles.EVENTS_MANAGER, AppRoles.LINKS_MANAGER], + }, + { + keyId: 'key456', + description: 'Test API Key 2', + owner: 'other@example.com', + createdAt: Math.floor(Date.now() / 1000) - 86400 * 7, // 7 days ago + expiresAt: undefined, // never expires + roles: [AppRoles.EVENTS_MANAGER], + }, + ]; + + const renderComponent = async () => { + await act(async () => { + render( + + + + + + ); + }); + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders the table headers correctly', async () => { + getApiKeys.mockResolvedValue([]); + await renderComponent(); + + expect(screen.getByText('Key ID')).toBeInTheDocument(); + expect(screen.getByText('Description')).toBeInTheDocument(); + expect(screen.getByText('Owner')).toBeInTheDocument(); + expect(screen.getByText('Created')).toBeInTheDocument(); + expect(screen.getByText('Expires')).toBeInTheDocument(); + expect(screen.getByText('Permissions')).toBeInTheDocument(); + }); + + it('shows loading state initially', async () => { + getApiKeys.mockResolvedValue([]); + await renderComponent(); + + // Check for skeletons (loading state) + // Since we're using act, we need to look for the skeleton before it's replaced + expect(getApiKeys).toHaveBeenCalledTimes(1); + }); + + it('displays API keys when loaded', async () => { + getApiKeys.mockResolvedValue(mockApiKeys); + await renderComponent(); + + // Wait for loading to complete + await waitFor(() => { + expect(screen.getByText('acmuiuc_key123')).toBeInTheDocument(); + }); + + expect(screen.getByText('Test API Key 1')).toBeInTheDocument(); + expect(screen.getByText('You')).toBeInTheDocument(); // Current user's key + expect(screen.getByText('other@example.com')).toBeInTheDocument(); + expect(screen.getByText('Never')).toBeInTheDocument(); // For key that never expires + }); + + it('handles empty API key list', async () => { + getApiKeys.mockResolvedValue([]); + await renderComponent(); + + await waitFor(() => { + expect(screen.getByText('No API keys found.')).toBeInTheDocument(); + }); + }); + + it('shows notification on API key fetch error', async () => { + const notificationsMock = vi.spyOn(notifications, 'show'); + getApiKeys.mockRejectedValue(new Error('Failed to load')); + await renderComponent(); + + await waitFor(() => { + expect(notificationsMock).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'Error loading API keys', + color: 'red', + }) + ); + }); + }); + + it('allows selecting and deselecting rows', async () => { + getApiKeys.mockResolvedValue(mockApiKeys); + await renderComponent(); + const user = userEvent.setup(); + + // Wait for data to load + await waitFor(() => { + expect(screen.getByText('acmuiuc_key123')).toBeInTheDocument(); + }); + + // Find checkboxes and select first row + const checkboxes = screen.getAllByRole('checkbox'); + expect(checkboxes.length).toBeGreaterThan(1); // Header + rows + + // Select first row + await user.click(checkboxes[1]); // First row checkbox (index 0 is header) + + // Delete button should appear with count + expect(screen.getByText(/Delete 1 API Key/)).toBeInTheDocument(); + + // Deselect + await user.click(checkboxes[1]); + + // Delete button should disappear + expect(screen.queryByText(/Delete 1 API Key/)).not.toBeInTheDocument(); + }); + + it('allows selecting all rows with header checkbox', async () => { + getApiKeys.mockResolvedValue(mockApiKeys); + await renderComponent(); + const user = userEvent.setup(); + + // Wait for data to load + await waitFor(() => { + expect(screen.getByText('acmuiuc_key123')).toBeInTheDocument(); + }); + + // Check that header checkbox exists + const headerCheckbox = screen.getAllByRole('checkbox')[0]; // Header checkbox + expect(headerCheckbox).toBeInTheDocument(); + + // Click header checkbox + await act(async () => { + await user.click(headerCheckbox); + }); + + // Delete button should show count of all rows + const deleteButton = await screen.findByText(/Delete 2 API Keys/); + expect(deleteButton).toBeInTheDocument(); + + // Uncheck all + await act(async () => { + await user.click(headerCheckbox); + }); + + // Delete button should be gone + await waitFor(() => { + expect(screen.queryByText(/Delete/)).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/ui/pages/apiKeys/ManageKeysTable.tsx b/src/ui/pages/apiKeys/ManageKeysTable.tsx new file mode 100644 index 00000000..c22ba817 --- /dev/null +++ b/src/ui/pages/apiKeys/ManageKeysTable.tsx @@ -0,0 +1,449 @@ +import { + Badge, + Button, + Center, + Checkbox, + Code, + CopyButton, + Group, + List, + Modal, + MultiSelect, + Skeleton, + Table, + Text, + TextInput, +} from '@mantine/core'; +import { DateTimePicker } from '@mantine/dates'; +import { IconAlertCircle, IconEye, IconPlus, IconTrash } from '@tabler/icons-react'; +import React, { useEffect, useState } from 'react'; +import { apiKeyAllowedRoles, ApiKeyMaskedEntry, ApiKeyPostBody } from '@common/types/apiKey'; +import { useAuth } from '@ui/components/AuthContext'; +import { notifications } from '@mantine/notifications'; +import pluralize from 'pluralize'; +import dayjs from 'dayjs'; +import { AppRoles } from '@common/roles'; +import { BlurredTextDisplay } from '../../components/BlurredTextDisplay'; + +const HumanFriendlyDate = ({ date }: { date: number }) => { + return {dayjs(date * 1000).format('MMMM D, YYYY h:mm A')}; +}; + +interface OrgApiKeyTableProps { + getApiKeys: () => Promise; + deleteApiKeys: (ids: string[]) => Promise; + createApiKey: (data: ApiKeyPostBody) => Promise<{ apiKey: string }>; +} + +export const OrgApiKeyTable: React.FC = ({ + getApiKeys, + deleteApiKeys, + createApiKey, +}) => { + const [apiKeys, setApiKeys] = useState(null); + const [selected, setSelected] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [createModalOpen, setCreateModalOpen] = useState(false); + const [createdKey, setCreatedKey] = useState(null); + // New state for delete confirmation modal + const [deleteModalOpen, setDeleteModalOpen] = useState(false); + const [idsToDelete, setIdsToDelete] = useState([]); + // New state for view permissions modal + const [viewPermissionsModalOpen, setViewPermissionsModalOpen] = useState(false); + const [selectedKeyForPermissions, setSelectedKeyForPermissions] = + useState(null); + + const { userData } = useAuth(); + + const fetchKeys = async () => { + try { + setIsLoading(true); + const data = await getApiKeys(); + setApiKeys(data); + } catch (e) { + notifications.show({ + title: 'Error loading API keys', + message: 'Unable to fetch API keys. Try again later.', + color: 'red', + icon: , + }); + } finally { + setIsLoading(false); + } + }; + + const handleDelete = async (ids: string[]) => { + try { + await deleteApiKeys(ids); + notifications.show({ + title: 'Deleted', + message: `${pluralize('API key', ids.length, true)} deleted successfully.`, + color: 'green', + }); + setSelected([]); + fetchKeys(); + } catch (e) { + notifications.show({ + title: 'Delete failed', + message: 'Something went wrong while deleting the API keys.', + color: 'red', + icon: , + }); + } finally { + // Close the modal after deletion attempt + setDeleteModalOpen(false); + } + }; + + // New function to open the delete confirmation modal + const confirmDelete = (ids: string[]) => { + setIdsToDelete(ids); + setDeleteModalOpen(true); + }; + + // New function to open the view permissions modal + const openViewPermissionsModal = (key: ApiKeyMaskedEntry) => { + setSelectedKeyForPermissions(key); + setViewPermissionsModalOpen(true); + }; + + useEffect(() => { + fetchKeys(); + }, []); + + const handleCreate = async (form: ApiKeyPostBody) => { + try { + const res = await createApiKey(form); + setCreatedKey(res.apiKey); + setCreateModalOpen(false); + await fetchKeys(); + } catch (e) { + notifications.show({ + title: 'Create failed', + message: 'Unable to create API key.', + color: 'red', + }); + } + }; + + const createRow = (entry: ApiKeyMaskedEntry) => ( + + + + setSelected( + event.currentTarget.checked + ? [...selected, entry.keyId] + : selected.filter((id) => id !== entry.keyId) + ) + } + /> + + + acmuiuc_{entry.keyId} + + {entry.description} + {entry.owner === userData?.email ? 'You' : entry.owner} + + + + + {entry.expiresAt ? ( + + ) : ( + Never + )} + + + + + + + + ); + + // --- Create Form State --- + const [roles, setRoles] = useState([]); + const [description, setDescription] = useState(''); + const [expiresAt, setExpiresAt] = useState(null); + + return ( + <> + + + + {selected.length > 0 && ( + + )} + + + + + + + + + + setSelected( + event.currentTarget.checked && apiKeys ? apiKeys.map((k) => k.keyId) : [] + ) + } + /> + + Key ID + Description + Owner + Created + Expires + Permissions + + + + {isLoading || !apiKeys ? ( + [...Array(3)].map((_, i) => ( + + {Array(7) + .fill(0) + .map((_, idx) => ( + + + + ))} + + )) + ) : apiKeys.length === 0 ? ( + + +
+ + No API keys found. + +
+
+
+ ) : ( + apiKeys.map(createRow) + )} +
+
+
+ + All times shown in local timezone ({Intl.DateTimeFormat().resolvedOptions().timeZone}). + + + {/* Create Modal */} + setCreateModalOpen(false)} + title="Create API Key" + centered + > + { + setRoles(e as AppRoles[]); + }} + required + /> + setDescription(e.currentTarget.value)} + required + mt="md" + /> + + + + + + + {/* Created Key Modal */} + setCreatedKey(null)} + title="API Key Created!" + centered + > + + This is the only time you'll see this key. Store it securely. + + {createdKey ? ( + + ) : ( + 'An error occurred and your key cannot be displayed' + )} + + + {({ copied, copy }) => ( + + )} + + + + + {/* Delete Confirmation Modal */} + setDeleteModalOpen(false)} + title="Confirm Deletion" + centered + > + + Are you sure you want to delete the following API {pluralize('key', idsToDelete.length)}? + + + {pluralize('This', idsToDelete.length)} {pluralize('key', idsToDelete.length)} will + immediately be deactivated, and API requests using {pluralize('this', idsToDelete.length)}{' '} + {pluralize('key', idsToDelete.length)} will fail. + + + {idsToDelete.map((id) => ( + + acmuiuc_{id} + + ))} + + + + + + + + + {/* View Permissions Modal - Reusing components from create modal */} + setViewPermissionsModalOpen(false)} + title="API Key Permissions" + centered + > + {selectedKeyForPermissions && ( + <> + + Key ID + + acmuiuc_{selectedKeyForPermissions.keyId} + + + Description + + + {selectedKeyForPermissions.description} + + + + Roles + + + + + Created + + + + + + + Expires + + + {selectedKeyForPermissions.expiresAt ? ( + + ) : ( + 'Never' + )} + + + + Owner + + + {selectedKeyForPermissions.owner === userData?.email + ? 'You' + : selectedKeyForPermissions.owner} + + + {selectedKeyForPermissions.restrictions && ( + <> + + Policy Restrictions + + + {JSON.stringify(selectedKeyForPermissions.restrictions, null, 2)} + + + )} + + + + + + )} + + + ); +}; diff --git a/src/ui/pages/events/ManageEvent.page.tsx b/src/ui/pages/events/ManageEvent.page.tsx index ababcba2..ae6b5b49 100644 --- a/src/ui/pages/events/ManageEvent.page.tsx +++ b/src/ui/pages/events/ManageEvent.page.tsx @@ -1,4 +1,16 @@ -import { Title, Box, TextInput, Textarea, Switch, Select, Button, Loader } from '@mantine/core'; +import { + Title, + Box, + TextInput, + Textarea, + Switch, + Select, + Button, + Loader, + Group, + ActionIcon, + Text, +} from '@mantine/core'; import { DateTimePicker } from '@mantine/dates'; import { useForm, zodResolver } from '@mantine/form'; import { notifications } from '@mantine/notifications'; @@ -7,11 +19,17 @@ import React, { useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { z } from 'zod'; import { AuthGuard } from '@ui/components/AuthGuard'; -import { getRunEnvironmentConfig } from '@ui/config'; import { useApi } from '@ui/util/api'; import { OrganizationList as orgList } from '@common/orgs'; import { AppRoles } from '@common/roles'; import { EVENT_CACHED_DURATION } from '@common/config'; +import { IconPlus, IconTrash } from '@tabler/icons-react'; +import { + MAX_METADATA_KEYS, + MAX_KEY_LENGTH, + MAX_VALUE_LENGTH, + metadataSchema, +} from '@common/types/events'; export function capitalizeFirstLetter(string: string) { return string.charAt(0).toUpperCase() + string.slice(1); @@ -29,6 +47,8 @@ const baseBodySchema = z.object({ host: z.string().min(1, 'Host is required'), featured: z.boolean().default(false), paidEventId: z.string().min(1, 'Paid Event ID must be at least 1 character').optional(), + // Add metadata field + metadata: metadataSchema, }); const requestBodySchema = baseBodySchema @@ -68,6 +88,7 @@ export const ManageEventPage: React.FC = () => { try { const response = await api.get(`/api/v1/events/${eventId}?ts=${Date.now()}`); const eventData = response.data; + const formValues = { title: eventData.title, description: eventData.description, @@ -80,6 +101,7 @@ export const ManageEventPage: React.FC = () => { repeats: eventData.repeats, repeatEnds: eventData.repeatEnds ? new Date(eventData.repeatEnds) : undefined, paidEventId: eventData.paidEventId, + metadata: eventData.metadata || {}, }; form.setValues(formValues); } catch (error) { @@ -107,8 +129,10 @@ export const ManageEventPage: React.FC = () => { repeats: undefined, repeatEnds: undefined, paidEventId: undefined, + metadata: {}, // Initialize empty metadata object }, }); + useEffect(() => { if (form.values.end && form.values.end <= form.values.start) { form.setFieldValue('end', new Date(form.values.start.getTime() + 3.6e6)); // 1 hour after the start date @@ -124,6 +148,7 @@ export const ManageEventPage: React.FC = () => { const handleSubmit = async (values: EventPostRequest) => { try { setIsSubmitting(true); + const realValues = { ...values, start: dayjs(values.start).format('YYYY-MM-DD[T]HH:mm:00'), @@ -133,6 +158,7 @@ export const ManageEventPage: React.FC = () => { ? dayjs(values.repeatEnds).format('YYYY-MM-DD[T]HH:mm:00') : undefined, repeats: values.repeats ? values.repeats : undefined, + metadata: Object.keys(values.metadata || {}).length > 0 ? values.metadata : undefined, }; const eventURL = isEditing ? `/api/v1/events/${eventId}` : '/api/v1/events'; @@ -151,6 +177,87 @@ export const ManageEventPage: React.FC = () => { } }; + // Function to add a new metadata field + const addMetadataField = () => { + const currentMetadata = { ...form.values.metadata }; + if (Object.keys(currentMetadata).length >= MAX_METADATA_KEYS) { + notifications.show({ + message: `You can add at most ${MAX_METADATA_KEYS} metadata keys.`, + }); + return; + } + + // Generate a temporary key name that doesn't exist yet + let tempKey = `key${Object.keys(currentMetadata).length + 1}`; + // Make sure it's unique + while (currentMetadata[tempKey] !== undefined) { + tempKey = `key${parseInt(tempKey.replace('key', '')) + 1}`; + } + + // Update the form + form.setValues({ + ...form.values, + metadata: { + ...currentMetadata, + [tempKey]: '', + }, + }); + }; + + // Function to update a metadata value + const updateMetadataValue = (key: string, value: string) => { + form.setValues({ + ...form.values, + metadata: { + ...form.values.metadata, + [key]: value, + }, + }); + }; + + const updateMetadataKey = (oldKey: string, newKey: string) => { + const metadata = { ...form.values.metadata }; + if (oldKey === newKey) return; + + const value = metadata[oldKey]; + delete metadata[oldKey]; + metadata[newKey] = value; + + form.setValues({ + ...form.values, + metadata, + }); + }; + + // Function to remove a metadata field + const removeMetadataField = (key: string) => { + const currentMetadata = { ...form.values.metadata }; + delete currentMetadata[key]; + + form.setValues({ + ...form.values, + metadata: currentMetadata, + }); + }; + + const [metadataKeys, setMetadataKeys] = useState>({}); + + // Initialize metadata keys with unique IDs when form loads or changes + useEffect(() => { + const newMetadataKeys: Record = {}; + + // For existing metadata, create stable IDs + Object.keys(form.values.metadata || {}).forEach((key) => { + if (!metadataKeys[key]) { + newMetadataKeys[key] = `meta-${Math.random().toString(36).substring(2, 9)}`; + } else { + newMetadataKeys[key] = metadataKeys[key]; + } + }); + + setMetadataKeys(newMetadataKeys); + }, [Object.keys(form.values.metadata || {}).length]); + return ( @@ -230,6 +337,71 @@ export const ManageEventPage: React.FC = () => { placeholder="Enter Ticketing ID or Merch ID prefixed with merch:" {...form.getInputProps('paidEventId')} /> + + {/* Metadata Section */} + + Metadata + + + + + These values can be acceessed via the API. Max {MAX_KEY_LENGTH} characters for keys + and {MAX_VALUE_LENGTH} characters for values. + + + {Object.entries(form.values.metadata || {}).map(([key, value], index) => { + const keyError = key.trim() === '' ? 'Key is required' : undefined; + const valueError = value.trim() === '' ? 'Value is required' : undefined; + + return ( + + updateMetadataKey(key, e.currentTarget.value)} + error={keyError} + style={{ flex: 1 }} + /> + + updateMetadataValue(key, e.currentTarget.value)} + error={valueError} + /> + {/* Empty space to maintain consistent height */} + {valueError &&
} + + removeMetadataField(key)} + mt={30} // align with inputs when label is present + > + + + + ); + })} + + {Object.keys(form.values.metadata || {}).length > 0 && ( + + + {Object.keys(form.values.metadata || {}).length} of {MAX_METADATA_KEYS} fields + used + + + )} + + */} + + + + + + )} + + ); + }; + + const renderDelegatedLinks = (link: LinkryGetResponse, index: number) => { + const shouldShow = true; + + return ( + + {(styles) => ( + + + + {' '} + {/* Currently set to localhost for local testing purposes */} + https://go.acm.illinois.edu/{link.slug} + + + + + {link.redirect} + + + + {link.access.map((group, index) => ( + + {group.trim()} {/* Trim any extra whitespace */} + + ))} + + {/* {dayjs(link.createdAt).format('MMM D YYYY hh:mm')} + {dayjs(link.updatedAt).format('MMM D YYYY hh:mm')} */} + + + {/* */} + + + + + + )} + + ); + }; + + useEffect(() => { + const getEvents = async () => { + setIsLoading(true); + let response; + try { + response = await api.get('/api/v1/linkry/redir'); + } catch (e) { + throw e; + } finally { + setIsLoading(false); + } + const ownedLinks = response.data.ownedLinks; + const delegatedLinks = response.data.delegatedLinks; + setOwnedLinks(ownedLinks); + setDelegatedLinks(delegatedLinks); + }; + getEvents(); + }, []); + + const deleteLink = async (slug: string) => { + try { + const encodedSlug = encodeURIComponent(slug); + setIsLoading(true); + try { + await api.delete(`/api/v1/linkry/redir/${encodedSlug}`); + } catch (e) { + throw e; + } finally { + setIsLoading(false); + } + setOwnedLinks((prevLinks) => prevLinks.filter((link) => link.slug !== slug)); + setDelegatedLinks((prevLinks) => prevLinks.filter((link) => link.slug !== slug)); + setIsLoading(false); + notifications.show({ + title: 'Link deleted', + message: 'The link was deleted successfully.', + }); + close(); + } catch (error) { + console.error(error); + notifications.show({ + title: 'Error deleting event', + message: `${error}`, + color: 'red', + }); + } + }; + + return ( + + + + + {deleteLinkCandidate && ( + { + setDeleteLinkCandidate(null); + close(); + }} + title="Confirm Deletion" + > + + Are you sure you want to delete the redirect from{' '} + {deleteLinkCandidate.slug} to {deleteLinkCandidate.redirect}? + +
+ + + + +
+ )} + + User Links + + +
+ +
+ + + + My Links + Delegated Links + + + +
+ + + + Shortened Link + Redirect URL + Actions + + + {ownedLinks.map(renderTableRow)} +
+
+
+ + +
+ + + + Shortened Link + Redirect URL + Actions + + + {delegatedLinks.map(renderTableRow)} +
+
+
+
+
+ ); +}; diff --git a/src/ui/pages/linkry/ManageLink.page.tsx b/src/ui/pages/linkry/ManageLink.page.tsx new file mode 100644 index 00000000..f06c3ef7 --- /dev/null +++ b/src/ui/pages/linkry/ManageLink.page.tsx @@ -0,0 +1,265 @@ +import { + Title, + Box, + TextInput, + Textarea, + Switch, + Select, + Button, + Loader, + TextInputProps, + Group, + getSize, + Container, +} from '@mantine/core'; +import { DateTimePicker } from '@mantine/dates'; +import { useForm, zodResolver } from '@mantine/form'; +import { MultiSelect } from '@mantine/core'; +import { notifications } from '@mantine/notifications'; +import dayjs from 'dayjs'; +import React, { useEffect, useRef, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { z } from 'zod'; +import { AuthGuard } from '@ui/components/AuthGuard'; +import { getRunEnvironmentConfig } from '@ui/config'; +import { useApi } from '@ui/util/api'; +import { OrganizationList as orgList } from '@common/orgs'; +import { AppRoles } from '@common/roles'; +import { IconCancel, IconCross, IconDeviceFloppy, IconScale } from '@tabler/icons-react'; +import { environmentConfig, LinkryGroupUUIDToGroupNameMap } from '@common/config'; +import FullScreenLoader from '@ui/components/AuthContext/LoadingScreen'; +import { AxiosError } from 'axios'; +import { useAuth } from '@ui/components/AuthContext'; +import { LINKRY_MAX_SLUG_LENGTH } from '@common/types/linkry'; + +export function capitalizeFirstLetter(string: string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} + +const baseUrl = 'go.acm.illinois.edu'; +const slugRegex = new RegExp('^(https?://)?[a-zA-Z0-9-._/]*$'); +const urlRegex = new RegExp('^https?://[a-zA-Z0-9-._/?=&+:]*$'); + +const baseBodySchema = z + .object({ + slug: z + .string() + .min(1, 'Enter or generate an alias') + .regex( + slugRegex, + "Invalid input: Only alphanumeric characters, '-', '_', '/', and '.' are allowed" + ) + .optional(), + access: z.array(z.string()).optional(), + redirect: z + .string() + .min(1) + .regex(urlRegex, 'Invalid URL. Use format: https:// or https://www.example.com') + .optional(), + createdAt: z.number().optional(), + updatedAt: z.number().optional(), + counter: z.number().optional(), + }) + .superRefine((data, ctx) => { + if ((data.slug?.length || 0) > LINKRY_MAX_SLUG_LENGTH) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['slug'], + message: 'Shortened URL cannot be that long', + }); //Throw custom error through context using superrefine + } + }); + +const requestBodySchema = baseBodySchema; + +type LinkPostRequest = z.infer; + +export function getFilteredUserGroups(groups: string[]) { + return groups.filter((groupId) => [...LinkryGroupUUIDToGroupNameMap.keys()].includes(groupId)); +} + +export const ManageLinkPage: React.FC = () => { + const [isSubmitting, setIsSubmitting] = useState(false); + + const [isLoading, setIsLoading] = useState(false); + + const [isEdited, setIsEdited] = useState(false); // Track if the form is edited + + const navigate = useNavigate(); + const api = useApi('core'); + + const { slug } = useParams(); + + const isEditing = slug !== undefined; + + useEffect(() => { + if (!isEditing) { + return; + } + // Fetch event data and populate form + const startForm = async () => { + try { + setIsLoading(true); + const response = await api.get(`/api/v1/linkry/redir/${slug}`); + const linkData = response.data; + const formValues = { + slug: linkData.slug, + access: linkData.access, + redirect: linkData.redirect, + }; + form.setValues(formValues); + setIsLoading(false); + } catch (error) { + console.error('Error fetching event data:', error); + notifications.show({ + message: 'Failed to fetch event data, please try again.', + }); + navigate('/linkry'); + } + }; + // decode JWT to get user groups + startForm(); + }, []); + + const form = useForm({ + validate: zodResolver(requestBodySchema), + initialValues: { + slug: '', + access: [], + redirect: '', + }, + }); + + const handleSubmit = async (values: LinkPostRequest) => { + /*if (!values.access || values.redirect || !values.slug){ + notifications.show({ + message: "Please fill in all entries", + }); + } */ //Potential warning for fields that are not filled... + let response; + try { + setIsSubmitting(true); + const realValues = { + ...values, + isEdited: isEdited, + }; + + response = await api.post('/api/v1/linkry/redir', realValues); + notifications.show({ + message: isEditing ? 'Link updated!' : 'Link created!', + }); + navigate(new URLSearchParams(window.location.search).get('previousPage') || '/linkry'); + } catch (error: any) { + setIsSubmitting(false); + console.error('Error creating/editing link:', error); + notifications.show({ + color: 'red', + title: isEditing ? 'Failed to edit link' : 'Failed to create link', + message: error.response && error.response.data ? error.response.data.message : undefined, + }); + } + }; + + const handleFormClose = () => { + navigate(new URLSearchParams(window.location.search).get('previousPage') || '/linkry'); + }; + + const generateRandomSlug = () => { + const randomSlug = Array.from( + { length: 6 }, + () => 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'[Math.floor(Math.random() * 52)] + ).join(''); + form.setFieldValue('slug', randomSlug); + }; + + const handleSlug = (event: React.ChangeEvent) => { + form.setFieldValue('slug', event.currentTarget.value); + }; + + const handleFormChange = () => { + setIsEdited(true); // Set the flag to true when any field is changed + }; + + if (isLoading) { + return ; + } + + return ( + + + {isEditing ? 'Edit' : 'Add'} Link + +
+ + {baseUrl || 'go.acm.illinois.edu'} + + } + rightSection={ + !isEditing && ( + + ) + } + mt="xl" + {...{ ...form.getInputProps('slug'), onChange: handleSlug }} + disabled={isEditing} + onChange={(e) => { + form.getInputProps('slug').onChange(e); + handleFormChange(); // Mark as edited + }} + /> + { + form.getInputProps('redirect').onChange(e); + handleFormChange(); // Mark as edited + }} + /> + ({ + value: x, + label: LinkryGroupUUIDToGroupNameMap.get(x) || x, + })) ?? [] + } + value={form.values.access} + onChange={(value) => { + form.setFieldValue('access', value); + handleFormChange(); + }} + mt="xl" + /> + + +
+
+
+ ); +}; diff --git a/src/ui/pages/logs/LogRenderer.test.tsx b/src/ui/pages/logs/LogRenderer.test.tsx new file mode 100644 index 00000000..f67d8539 --- /dev/null +++ b/src/ui/pages/logs/LogRenderer.test.tsx @@ -0,0 +1,290 @@ +import React from 'react'; +import { render, screen, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { vi } from 'vitest'; +import { MantineProvider } from '@mantine/core'; +import { notifications } from '@mantine/notifications'; +import { LogRenderer } from './LogRenderer'; +import { Modules, ModulesToHumanName } from '@common/modules'; +import { MemoryRouter } from 'react-router-dom'; + +describe('LogRenderer Tests', () => { + const getLogsMock = vi.fn(); + console.log('matchMedia in test:', window.matchMedia?.toString()); + + // Mock date for consistent testing + const mockCurrentDate = new Date('2023-01-15T12:00:00Z'); + const mockPastDate = new Date('2023-01-14T12:00:00Z'); + + // Sample log data for testing + const sampleLogs = [ + { + actor: 'admin', + createdAt: Math.floor(mockCurrentDate.getTime() / 1000) - 3600, + expireAt: Math.floor(mockCurrentDate.getTime() / 1000) + 86400, + message: 'User created', + module: Modules.IAM, + requestId: 'req-123', + target: 'user@example.com', + }, + { + actor: 'system', + createdAt: Math.floor(mockCurrentDate.getTime() / 1000) - 7200, + expireAt: Math.floor(mockCurrentDate.getTime() / 1000) + 86400, + message: 'Config updated', + module: Modules.AUDIT_LOG, + requestId: 'req-456', + target: Modules.STRIPE, + }, + ]; + + const renderComponent = async () => { + await act(async () => { + render( + + + + + + ); + }); + }; + + beforeEach(() => { + vi.clearAllMocks(); + // Mock Date.now to return a fixed timestamp + vi.spyOn(Date, 'now').mockImplementation(() => mockCurrentDate.getTime()); + // Reset notification spy + vi.spyOn(notifications, 'show'); + }); + + it('renders the filter controls correctly', async () => { + await renderComponent(); + + expect(screen.getByText('Filter Logs')).toBeInTheDocument(); + expect(screen.getByText('Module')).toBeInTheDocument(); + expect(screen.getByText('Start Time')).toBeInTheDocument(); + expect(screen.getByText('End Time')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Fetch Logs/i })).toBeInTheDocument(); + }); + + it('shows error notification when fetch logs without selecting a module', async () => { + const user = userEvent.setup(); + await renderComponent(); + + await user.click(screen.getByRole('button', { name: /Fetch Logs/i })); + + expect(notifications.show).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'Missing parameters', + message: 'Please select a module and time range', + color: 'red', + }) + ); + expect(getLogsMock).not.toHaveBeenCalled(); + }); + + it('fetches logs successfully when parameters are valid', async () => { + getLogsMock.mockResolvedValue(sampleLogs); + const user = userEvent.setup(); + await renderComponent(); + + // Select a module + await user.click(screen.getByPlaceholderText('Select service module')); + // Find and click on the IAM option + await user.click(screen.getByText(ModulesToHumanName[Modules.IAM])); + + // Click fetch logs + await user.click(screen.getByRole('button', { name: /Fetch Logs/i })); + + // Verify the getLogs was called with correct parameters + expect(getLogsMock).toHaveBeenCalledWith( + Modules.IAM, + expect.any(Number), // Start timestamp + expect.any(Number) // End timestamp + ); + + // Verify logs are displayed + await screen.findByText('User created'); + expect(screen.getByText('admin')).toBeInTheDocument(); + expect(screen.getByText('user@example.com')).toBeInTheDocument(); + expect(screen.getByText('req-123')).toBeInTheDocument(); + }); + + it('handles API errors gracefully', async () => { + getLogsMock.mockRejectedValue(new Error('API Error')); + const user = userEvent.setup(); + await renderComponent(); + + // Select a module + await user.click(screen.getByPlaceholderText('Select service module')); + await user.click(screen.getByText(ModulesToHumanName[Modules.EVENTS])); + + // Click fetch logs + await user.click(screen.getByRole('button', { name: /Fetch Logs/i })); + + expect(notifications.show).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'Error fetching logs', + message: 'Failed to load logs. Please try again later.', + color: 'red', + }) + ); + }); + + it('filters logs based on search query', async () => { + getLogsMock.mockResolvedValue(sampleLogs); + const user = userEvent.setup(); + await renderComponent(); + + // Select a module and fetch logs + await user.click(screen.getByPlaceholderText('Select service module')); + await user.click(screen.getByText(ModulesToHumanName[Modules.AUDIT_LOG])); + await user.click(screen.getByRole('button', { name: /Fetch Logs/i })); + + // Wait for logs to display + await screen.findByText('User created'); + + // Search for 'config' + await user.type(screen.getByPlaceholderText('Search in logs...'), 'config'); + + // "User created" should no longer be visible, but "Config updated" should be + expect(screen.queryByText('User created')).not.toBeInTheDocument(); + expect(screen.getByText('Config updated')).toBeInTheDocument(); + }); + + it('toggles between UTC and local time display', async () => { + getLogsMock.mockResolvedValue(sampleLogs); + const user = userEvent.setup(); + await renderComponent(); + + // Select a module and fetch logs + await user.click(screen.getByPlaceholderText('Select service module')); + await user.click(screen.getByText(ModulesToHumanName[Modules.IAM])); + await user.click(screen.getByRole('button', { name: /Fetch Logs/i })); + + // Wait for logs to display + await screen.findByText('User created'); + + // Check default is local time + expect(screen.getByText(/Show times in local timezone/)).toBeInTheDocument(); + + // Toggle to UTC + await user.click(screen.getByRole('switch')); + expect(screen.getByText(/Show times in UTC/)).toBeInTheDocument(); + }); + + it('paginates logs correctly', async () => { + // Create 15 sample logs + const manyLogs = Array(15) + .fill(null) + .map((_, index) => ({ + actor: `actor-${index}`, + createdAt: Math.floor(mockCurrentDate.getTime() / 1000) - index * 100, + expireAt: Math.floor(mockCurrentDate.getTime() / 1000) + 86400, + message: `Message ${index}`, + module: Modules.IAM, + requestId: `req-${index}`, + target: `target-${index}`, + })); + + getLogsMock.mockResolvedValue(manyLogs); + const user = userEvent.setup(); + await renderComponent(); + + // Select a module and fetch logs + await user.click(screen.getByPlaceholderText('Select service module')); + await user.click(screen.getByText(ModulesToHumanName[Modules.IAM])); + await user.click(screen.getByRole('button', { name: /Fetch Logs/i })); + + // Wait for logs to display - first page should show entries 0-9 + await screen.findByText('Message 0'); + expect(screen.getByText('Message 9')).toBeInTheDocument(); + expect(screen.queryByText('Message 10')).not.toBeInTheDocument(); + + // Go to page 2 + await user.click(screen.getByRole('button', { name: '2' })); + + // Second page should show entries 10-14 + expect(screen.queryByText('Message 9')).not.toBeInTheDocument(); + expect(screen.getByText('Message 10')).toBeInTheDocument(); + expect(screen.getByText('Message 14')).toBeInTheDocument(); + + // Change page size + await user.click(screen.getByText('10')); + await user.click(screen.getByText('25')); + + // Should now show all logs on one page + expect(screen.getByText('Message 0')).toBeInTheDocument(); + expect(screen.getByText('Message 14')).toBeInTheDocument(); + }); + + it('shows empty state when no logs are returned', async () => { + getLogsMock.mockResolvedValue([]); + const user = userEvent.setup(); + await renderComponent(); + + // Select a module and fetch logs + await user.click(screen.getByPlaceholderText('Select service module')); + await user.click(screen.getByText(ModulesToHumanName[Modules.MOBILE_WALLET])); + await user.click(screen.getByRole('button', { name: /Fetch Logs/i })); + + // Should show empty state + expect(screen.getByText('No logs to display')).toBeInTheDocument(); + }); + + it('displays translated module names when viewing audit logs', async () => { + const auditLogs = [ + { + actor: 'admin', + createdAt: Math.floor(mockCurrentDate.getTime() / 1000) - 3600, + expireAt: Math.floor(mockCurrentDate.getTime() / 1000) + 86400, + message: 'Module accessed', + module: Modules.AUDIT_LOG, + requestId: 'req-789', + target: Modules.STRIPE, // This should be translated to "Stripe" in the UI + }, + ]; + + getLogsMock.mockResolvedValue(auditLogs); + const user = userEvent.setup(); + await renderComponent(); + + // Select the AUDIT_LOG module + await user.click(screen.getByPlaceholderText('Select service module')); + await user.click(screen.getByText(ModulesToHumanName[Modules.AUDIT_LOG])); + await user.click(screen.getByRole('button', { name: /Fetch Logs/i })); + + // Wait for logs to display + await screen.findByText('Module accessed'); + + // The target column should show "Stripe" (the human-readable name) instead of "stripe" + expect(screen.getAllByText('Stripe Integration')).toHaveLength(2); + }); + + it('respects date range selection when fetching logs', async () => { + getLogsMock.mockResolvedValue(sampleLogs); + const user = userEvent.setup(); + await renderComponent(); + + // Select a module + await user.click(screen.getByPlaceholderText('Select service module')); + await user.click(screen.getByText(ModulesToHumanName[Modules.LINKRY])); + + // Open and set Start Time + await user.click(screen.getByRole('button', { name: /Start Time/i })); + const [startInput] = await screen.findAllByRole('textbox'); + await user.type(startInput, '01/10/2023 12:00 AM'); + + // Open and set End Time + await user.click(screen.getByRole('button', { name: /End Time/i })); + const [endInput] = await screen.findAllByRole('textbox'); + await user.type(endInput, '01/11/2023 11:59 PM'); + + // Click Fetch Logs + await user.click(screen.getByRole('button', { name: /Fetch Logs/i })); + + // Assert that getLogsMock was called with correct arguments + expect(getLogsMock).toHaveBeenCalledWith('linkry', expect.any(Number), expect.any(Number)); + }); +}); diff --git a/src/ui/pages/logs/LogRenderer.tsx b/src/ui/pages/logs/LogRenderer.tsx new file mode 100644 index 00000000..e0f2b5e2 --- /dev/null +++ b/src/ui/pages/logs/LogRenderer.tsx @@ -0,0 +1,355 @@ +import React, { useState, useEffect } from 'react'; +import { + Table, + Group, + Select, + Title, + Paper, + Badge, + Text, + Button, + Pagination, + Loader, + Stack, + TextInput, + Switch, + Tooltip, +} from '@mantine/core'; +import { DateTimePicker } from '@mantine/dates'; +import { IconRefresh, IconFileText, IconSearch, IconClock, IconWorld } from '@tabler/icons-react'; +import { Modules, ModulesToHumanName } from '@common/modules'; +import { notifications } from '@mantine/notifications'; + +interface LogEntry { + actor: string; + createdAt: number; + expireAt: number; + message: string; + module: string; + requestId?: string; + target: string; +} + +interface LogRendererProps { + getLogs: (service: Modules, start: number, end: number) => Promise[]>; +} + +export const LogRenderer: React.FC = ({ getLogs }) => { + // State for selected time range + const [startTime, setStartTime] = useState( + new Date(Date.now() - 24 * 60 * 60 * 1000) // Default to 24 hours ago + ); + const [endTime, setEndTime] = useState(new Date()); + + // State for selected module + const [selectedModule, setSelectedModule] = useState(null); + + // State for logs and loading + const [logs, setLogs] = useState(null); + const [loading, setLoading] = useState(false); + + // State for pagination + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState('10'); + const pageSizeOptions = ['10', '25', '50', '100']; + + // Search filter + const [searchQuery, setSearchQuery] = useState(''); + + // Time display preference + const [showUtcTime, setShowUtcTime] = useState(false); + + // Convert Modules enum to array for Select component + const moduleOptions = Object.values(Modules) + .map((module) => ({ + value: module, + label: ModulesToHumanName[module], + })) + .sort((a, b) => a.label.localeCompare(b.label)); + // Convert local date to UTC epoch timestamp (seconds, not milliseconds) + const dateToEpochTimestamp = (date: Date): number => { + return Math.floor(date.getTime() / 1000); // Convert milliseconds to seconds + }; + + const fetchLogs = async () => { + if (!selectedModule || !startTime || !endTime) { + notifications.show({ + title: 'Missing parameters', + message: 'Please select a module and time range', + color: 'red', + }); + return; + } + + setLoading(true); + try { + // Convert the local dates to epoch timestamps in seconds + const startTimestamp = dateToEpochTimestamp(startTime); + const endTimestamp = dateToEpochTimestamp(endTime); + + const data = await getLogs(selectedModule as Modules, startTimestamp, endTimestamp); + + setLogs(data as LogEntry[]); + setCurrentPage(1); // Reset to first page on new data + } catch (error) { + console.error('Error fetching logs:', error); + notifications.show({ + title: 'Error fetching logs', + message: 'Failed to load logs. Please try again later.', + color: 'red', + }); + } finally { + setLoading(false); + } + }; + + // Filter logs based on search query + const filteredLogs = logs + ? logs.filter((log) => { + if (!searchQuery.trim()) return true; + + const query = searchQuery.toLowerCase(); + return ( + log.actor?.toLowerCase().includes(query) || + log.message?.toLowerCase().includes(query) || + log.target?.toLowerCase().includes(query) || + log.requestId?.toLowerCase().includes(query) + ); + }) + : []; + + // Calculate pagination + const totalItems = filteredLogs.length; + const totalPages = Math.ceil(totalItems / parseInt(pageSize)); + const startIndex = (currentPage - 1) * parseInt(pageSize); + const endIndex = startIndex + parseInt(pageSize); + const currentLogs = filteredLogs.slice(startIndex, endIndex); + + // Format timestamp to readable date based on user preference + const formatTimestamp = (timestamp: number): string => { + // Multiply by 1000 to convert from seconds to milliseconds if needed + const timeMs = timestamp > 10000000000 ? timestamp : timestamp * 1000; + const date = new Date(timeMs); + + if (showUtcTime) { + // Format in UTC time + return date.toLocaleString(undefined, { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: true, + timeZone: 'UTC', + timeZoneName: 'short', + }); + } else { + // Format in local time with timezone name + return new Date(timeMs).toLocaleString(undefined, { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + timeZoneName: 'short', + hour12: true, + }); + } + }; + + // Get relative time from now + const getRelativeTime = (timestamp: number): string => { + // Ensure timestamp is in milliseconds + const timeMs = timestamp > 10000000000 ? timestamp : timestamp * 1000; + const now = Date.now(); + const diff = now - timeMs; + + const seconds = Math.floor(diff / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (days > 0) return `${days} day${days > 1 ? 's' : ''} ago`; + if (hours > 0) return `${hours} hour${hours > 1 ? 's' : ''} ago`; + if (minutes > 0) return `${minutes} minute${minutes > 1 ? 's' : ''} ago`; + return `${seconds} second${seconds !== 1 ? 's' : ''} ago`; + }; + + return ( + + + + Filter Logs + + + { + setPageSize(value || '10'); + setCurrentPage(1); + }} + data={pageSizeOptions} + style={{ width: 80 }} + /> + + Showing {startIndex + 1} to {Math.min(endIndex, totalItems)} of {totalItems} entries + + + + + + ) : logs === null ? null : ( + + + + + No logs to display + + {selectedModule + ? "Select a new time range and click 'Fetch Logs'" + : 'Select a module and time range to fetch logs'} + + + + + )} + + ); +}; diff --git a/src/ui/pages/logs/ViewLogs.page.tsx b/src/ui/pages/logs/ViewLogs.page.tsx new file mode 100644 index 00000000..23c87fcb --- /dev/null +++ b/src/ui/pages/logs/ViewLogs.page.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Container, Title, Text } from '@mantine/core'; +import { AuthGuard } from '@ui/components/AuthGuard'; +import { AppRoles } from '@common/roles'; +import { Modules } from '@common/modules'; +import { useApi } from '@ui/util/api'; +import { LogRenderer } from './LogRenderer'; // Adjust import path as needed + +export const ViewLogsPage: React.FC = () => { + const api = useApi('core'); + + const getLogs = async ( + service: Modules, + start: number, + end: number + ): Promise[]> => { + const response = await api.get(`/api/v1/logs/${service}?start=${start}&end=${end}`); + return response.data; + }; + + return ( + + + Audit Logs + + View system activity logs across different services + + + + + + ); +}; diff --git a/src/ui/pages/roomRequest/RoomRequestLanding.page.tsx b/src/ui/pages/roomRequest/RoomRequestLanding.page.tsx index 1764e3ee..ddfa49ea 100644 --- a/src/ui/pages/roomRequest/RoomRequestLanding.page.tsx +++ b/src/ui/pages/roomRequest/RoomRequestLanding.page.tsx @@ -22,7 +22,7 @@ export const ManageRoomRequestsPage: React.FC = () => { const createRoomRequest = async ( payload: RoomRequestFormValues ): Promise => { - const response = await api.post(`/api/v1/roomRequests/`, payload); + const response = await api.post(`/api/v1/roomRequests`, payload); return response.data; }; diff --git a/src/ui/pages/siglead/ManageSigLeads.page.tsx b/src/ui/pages/siglead/ManageSigLeads.page.tsx index 4b5fda39..4420f50b 100644 --- a/src/ui/pages/siglead/ManageSigLeads.page.tsx +++ b/src/ui/pages/siglead/ManageSigLeads.page.tsx @@ -22,6 +22,9 @@ import { useApi } from '@ui/util/api'; import { OrganizationList as orgList } from '@common/orgs'; import { AppRoles } from '@common/roles'; import { ScreenComponent } from './SigScreenComponents'; +import { GroupMemberGetResponse } from '@common/types/iam'; +import { transformCommaSeperatedName } from '@common/utils'; +import { orgsGroupId } from '@common/config'; export function capitalizeFirstLetter(string: string) { return string.charAt(0).toUpperCase() + string.slice(1); @@ -164,9 +167,42 @@ export const ManageSigLeadsPage: React.FC = () => { } }; + const getGroupMembers = async (selectedGroup: string) => { + try { + const response = await api.get(`/api/v1/iam/groups/${selectedGroup}`); + const data = response.data as GroupMemberGetResponse; + const responseMapped = data + .map((x) => ({ + ...x, + name: transformCommaSeperatedName(x.name), + })) + .sort((a, b) => (a.name > b.name ? 1 : a.name < b.name ? -1 : 0)); + // console.log(responseMapped); + return responseMapped; + } catch (error) { + console.error('Failed to get users:', error); + return []; + } + }; + + const TestButton: React.FC = () => { + return ( + + ); + }; + return ( + SigLead Management System {/* */} diff --git a/src/ui/pages/siglead/ViewSigLead.page.tsx b/src/ui/pages/siglead/ViewSigLead.page.tsx new file mode 100644 index 00000000..ca7af579 --- /dev/null +++ b/src/ui/pages/siglead/ViewSigLead.page.tsx @@ -0,0 +1,182 @@ +import { + Title, + Box, + TextInput, + Textarea, + Switch, + Select, + Button, + Loader, + Container, + Transition, + useMantineColorScheme, + Table, + Group, + Stack, +} from '@mantine/core'; +import { DateTimePicker } from '@mantine/dates'; +import { useForm, zodResolver } from '@mantine/form'; +import { notifications } from '@mantine/notifications'; +import dayjs from 'dayjs'; +import React, { useEffect, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { z } from 'zod'; +import { AuthGuard } from '@ui/components/AuthGuard'; +import { getRunEnvironmentConfig } from '@ui/config'; +import { useApi } from '@ui/util/api'; +import { AppRoles } from '@common/roles'; +import { SigDetailRecord, SigMemberRecord } from '@common/types/siglead.js'; + +export const ViewSigLeadPage: React.FC = () => { + const [isSubmitting, setIsSubmitting] = useState(false); + const navigate = useNavigate(); + const api = useApi('core'); + const { colorScheme } = useMantineColorScheme(); + const { sigId } = useParams(); + const [sigMembers, setSigMembers] = useState([]); + const [sigDetails, setSigDetails] = useState({ + sigid: sigId || '', + signame: 'Default Sig', + description: + 'A cool Sig with a lot of money and members. Founded in 1999 by Sir Charlie of Edinburgh. Focuses on making money and helping others earn more money via education.', + }); + + useEffect(() => { + // Fetch sig data and populate form + const getSig = async () => { + try { + /*const formValues = { + }; + form.setValues(formValues);*/ + const sigMemberRequest = await api.get(`/api/v1/siglead/sigmembers/${sigId}`); + setSigMembers(sigMemberRequest.data); + + const sigDetailRequest = await api.get(`/api/v1/siglead/sigdetail/${sigId}`); + setSigDetails(sigDetailRequest.data); + } catch (error) { + console.error('Error fetching sig data:', error); + notifications.show({ + message: 'Failed to fetch sig data, please try again.', + }); + } + }; + getSig(); + }, [sigId]); + + const renderSigMember = (member: SigMemberRecord, index: number) => { + const shouldShow = true; + return ( + + {(styles) => ( + + {member.memberName} + {member.email} + {member.designation} + + )} + + ); + }; + + /* + const form = useForm({ + validate: zodResolver(requestBodySchema), + initialValues: { + title: '', + description: '', + start: new Date(), + end: new Date(new Date().valueOf() + 3.6e6), // 1 hr later + location: 'ACM Room (Siebel CS 1104)', + locationLink: 'https://maps.app.goo.gl/dwbBBBkfjkgj8gvA8', + host: 'ACM', + featured: false, + repeats: undefined, + repeatEnds: undefined, + paidEventId: undefined, + }, + }); + /* + const handleSubmit = async (values: EventPostRequest) => { + try { + setIsSubmitting(true); + const realValues = { + ...values, + start: dayjs(values.start).format('YYYY-MM-DD[T]HH:mm:00'), + end: values.end ? dayjs(values.end).format('YYYY-MM-DD[T]HH:mm:00') : undefined, + repeatEnds: + values.repeatEnds && values.repeats + ? dayjs(values.repeatEnds).format('YYYY-MM-DD[T]HH:mm:00') + : undefined, + repeats: values.repeats ? values.repeats : undefined, + }; + + const eventURL = isEditing ? `/api/v1/events/${eventId}` : '/api/v1/events'; + const response = await api.post(eventURL, realValues); + notifications.show({ + title: isEditing ? 'Event updated!' : 'Event created!', + message: isEditing ? undefined : `The event ID is "${response.data.id}".`, + }); + navigate('/events/manage'); + } catch (error) { + setIsSubmitting(false); + console.error('Error creating/editing event:', error); + notifications.show({ + message: 'Failed to create/edit event, please try again.', + }); + } + };*/ + + return ( + + + + + {sigDetails.signame} + {sigDetails.description || ''} + + + + + + + + + + +
+ + + + Name + Email + Roles + + + + {sigMembers.length > 0 ? sigMembers.map(renderSigMember) : <>} + +
+
+
+
+ ); +}; diff --git a/src/ui/pages/tickets/SelectEventId.page.tsx b/src/ui/pages/tickets/SelectEventId.page.tsx index a5c8601f..9c470428 100644 --- a/src/ui/pages/tickets/SelectEventId.page.tsx +++ b/src/ui/pages/tickets/SelectEventId.page.tsx @@ -19,7 +19,7 @@ import FullScreenLoader from '@ui/components/AuthContext/LoadingScreen'; import { AuthGuard } from '@ui/components/AuthGuard'; import { useApi } from '@ui/util/api'; import { AppRoles } from '@common/roles'; -import App from '@ui/App'; +import { ItemPostData } from '@common/types/tickets'; const baseItemMetadata = z.object({ itemId: z.string().min(1), @@ -106,31 +106,29 @@ const SelectTicketsPage: React.FC = () => { const [reversedSort, setReversedSort] = useState(false); const api = useApi('core'); const navigate = useNavigate(); - + const fetchItems = async () => { + try { + setLoading(true); + const response = await api.get('/api/v1/tickets'); + const parsed = listItemsResponseSchema.parse(response.data); + setItems({ + tickets: parsed.tickets, + merch: parsed.merch, + }); + handleSort('status'); + } catch (error) { + console.error('Error fetching items:', error); + notifications.show({ + title: 'Error fetching items', + message: 'Failed to load available items. Please try again later.', + color: 'red', + }); + } finally { + setLoading(false); + } + }; useEffect(() => { - const fetchItems = async () => { - try { - setLoading(true); - const response = await api.get('/api/v1/tickets/'); - const parsed = listItemsResponseSchema.parse(response.data); - setItems({ - tickets: parsed.tickets, - merch: parsed.merch, - }); - } catch (error) { - console.error('Error fetching items:', error); - notifications.show({ - title: 'Error fetching items', - message: 'Failed to load available items. Please try again later.', - color: 'red', - }); - } finally { - setLoading(false); - } - }; - fetchItems(); - handleSort('status'); }, []); const handleSort = (field: SortBy) => { @@ -170,6 +168,37 @@ const SelectTicketsPage: React.FC = () => { return ; } + const handleToggleSales = async (item: ItemMetadata | TicketItemMetadata) => { + let newIsActive = false; + if (isTicketItem(item)) { + newIsActive = !(getTicketStatus(item).color === 'green'); + } else { + newIsActive = !(getMerchStatus(item).color === 'green'); + } + try { + setLoading(true); + const data: ItemPostData = { + itemSalesActive: newIsActive, + type: isTicketItem(item) ? 'ticket' : 'merch', + }; + await api.patch(`/api/v1/tickets/${item.itemId}`, data); + await fetchItems(); + notifications.show({ + title: 'Changes saved', + message: `Sales for ${item.itemName} are ${newIsActive ? 'enabled' : 'disabled'}!`, + }); + } catch (error) { + console.error('Error setting new status:', error); + notifications.show({ + title: 'Error setting status', + message: 'Failed to set status. Please try again later.', + color: 'red', + }); + } finally { + setLoading(false); + } + }; + const handleManageClick = (itemId: string) => { navigate(`/tickets/manage/${itemId}`); }; @@ -253,12 +282,19 @@ const SelectTicketsPage: React.FC = () => { resourceDef={{ service: 'core', validRoles: [AppRoles.TICKETS_MANAGER] }} > +
@@ -330,13 +366,22 @@ const SelectTicketsPage: React.FC = () => { isAppShell={false} resourceDef={{ service: 'core', validRoles: [AppRoles.TICKETS_MANAGER] }} > - + + + + diff --git a/src/ui/pages/tickets/ViewTickets.page.tsx b/src/ui/pages/tickets/ViewTickets.page.tsx index 5adeaa90..2bc1fc48 100644 --- a/src/ui/pages/tickets/ViewTickets.page.tsx +++ b/src/ui/pages/tickets/ViewTickets.page.tsx @@ -45,6 +45,12 @@ const getTicketStatus = ( return { status: 'unfulfilled', color: 'orange' }; }; +enum TicketsCopyMode { + ALL, + FULFILLED, + UNFULFILLED, +} + const ViewTicketsPage: React.FC = () => { const { eventId } = useParams(); const [allTickets, setAllTickets] = useState([]); @@ -57,6 +63,43 @@ const ViewTicketsPage: React.FC = () => { const [pageSize, setPageSize] = useState('10'); const pageSizeOptions = ['10', '25', '50', '100']; + const copyEmails = (mode: TicketsCopyMode) => { + try { + let emailsToCopy: string[] = []; + let copyModeHumanString = ''; + const nonRefundedTickets = allTickets.filter((x) => !x.refunded); + switch (mode) { + case TicketsCopyMode.ALL: + emailsToCopy = nonRefundedTickets.map((x) => x.purchaserData.email); + copyModeHumanString = 'All'; + break; + case TicketsCopyMode.FULFILLED: + emailsToCopy = nonRefundedTickets + .filter((x) => x.fulfilled) + .map((x) => x.purchaserData.email); + copyModeHumanString = 'Fulfilled'; + break; + case TicketsCopyMode.UNFULFILLED: + emailsToCopy = nonRefundedTickets + .filter((x) => !x.fulfilled) + .map((x) => x.purchaserData.email); + copyModeHumanString = 'Unfulfilled'; + break; + } + emailsToCopy = [...new Set(emailsToCopy)]; + navigator.clipboard.writeText(emailsToCopy.join(';')); + notifications.show({ + message: `${copyModeHumanString} emails copied!`, + }); + } catch (e) { + notifications.show({ + title: 'Failed to copy emails', + message: 'Please try again or contact support.', + color: 'red', + }); + } + }; + async function checkInUser(ticket: TicketEntry) { try { const response = await api.post(`/api/v1/tickets/checkIn`, { @@ -119,9 +162,34 @@ const ViewTicketsPage: React.FC = () => { return ( View Tickets/Merch Sales + + + + + + Note: all lists do not include refunded tickets.
-
- {pluralize('item', totalQuantitySold, true)} sold + + {pluralize('item', totalQuantitySold, true)} sold + diff --git a/src/ui/pages/tos/TermsOfService.page.tsx b/src/ui/pages/tos/TermsOfService.page.tsx new file mode 100644 index 00000000..3ead2dc3 --- /dev/null +++ b/src/ui/pages/tos/TermsOfService.page.tsx @@ -0,0 +1,592 @@ +import React from 'react'; +import { AcmAppShell } from '@ui/components/AppShell'; +import { + Title, + Text, + Container, + Stack, + Divider, + List, + Paper, + Group, + Anchor, + ThemeIcon, + Box, + Center, +} from '@mantine/core'; +import { + IconInfoCircle, + IconUser, + IconShield, + IconDatabase, + IconBrandOpenSource, + IconLicense, + IconAlertCircle, +} from '@tabler/icons-react'; + +export const TermsOfService: React.FC = () => { + return ( + <> + + + +
+ ACM @ UIUC Core Platform Terms of Service +
+
+ + Last Updated: April 21, 2025 + +
+ + + + + + + 1. Introduction + + + + These Terms of Service ("Terms") govern your access to and use of the ACM @ UIUC + Core Platform, including the Core API, user interface, and related developer tools, + documentation, and services (collectively, the "Core Platform") provided by the + University of Illinois/Urbana ACM Student Chapter ("ACM @ UIUC," "we," "our," or + "us"). + + + + 1.1 Agreement to Terms + + + By accessing or using the Core Platform, you agree to be bound by these Terms. If + you are using the Core Platform on behalf of an organization, you represent and + warrant that you have the authority to bind that organization to these Terms. + + + + 1.2 Open Source Project + + + The ACM @ UIUC Core Platform is an open source project. The source code is available + under the BSD 3-Clause License. These Terms govern your use of the Core Platform + services and interfaces, while the BSD 3-Clause License governs your use of the + source code. You are encouraged to review the source code, suggest improvements, + report issues, and contribute to the development of the project in accordance with + the project's contribution guidelines. + + + + + + + + + 2. License Grant and Restrictions + + + + 2.1 Platform Usage License + + + Subject to your compliance with these Terms, ACM @ UIUC grants you a limited, + non-exclusive, non-transferable, non-sublicensable, revocable license to access and + use the Core Platform solely for the purpose of developing, testing, and supporting + your application, website, or service that interfaces with the ACM @ UIUC Core + Platform. + + + + 2.2 Open Source Code License + + + The ACM @ UIUC Core Platform source code is available under the BSD 3-Clause + License. You can access, modify, and distribute the source code in accordance with + the terms of that license. The full text of the BSD 3-Clause License can be found in + the project repository. + + + + 2.3 Restrictions on Platform Usage + + + While using the Core Platform (as distinct from the source code), you shall not, and + shall not permit any third party to: + + + + Use the Core Platform in any manner that could damage, disable, overburden, or + impair the ACM @ UIUC Core Platform or interfere with any other party's use of the + Core Platform + + + Use the Core Platform to violate any applicable law, regulation, or third-party + rights + + + Use the Core Platform to develop applications primarily intended to replace the + ACM @ UIUC Core Platform's core functionality + + + Sell, lease, or sublicense direct access to the ACM @ UIUC Core Platform endpoints + themselves without adding substantial value + + + Use the Core Platform to scrape, mine, or gather user data in an unauthorized + manner + + + Attempt to bypass or circumvent any security measures or access limitations of the + Core Platform + + + Use the Core Platform in a manner that exceeds reasonable request volume or + constitutes excessive or abusive usage + + + Use the Core Platform for advertising or marketing purposes by (i) targeting ads + based on Platform data, or (ii) serving ads based on Platform data + + + Misrepresent your identity or the nature of your application when requesting + authorization from users or using the Core Platform + + + Request from the Core Platform more than the minimum amount of data, or more than + the minimum permissions to the types of data, that your application needs to + function properly + + + + + + + + + + 3. Authentication and Access + + + + 3.1 Authentication Methods + + Access to the Core Platform requires the use of either: + + + Bearer tokens obtained through the ACM @ UIUC Core Platform UI, or + + API tokens issued specifically for programmatic access + + + You are responsible for keeping all tokens secure and confidential. You may not + share your tokens with any third party or attempt to access the Core Platform using + tokens not issued to you. + + + + 3.2 Token Security + + + You must implement appropriate security measures to protect your tokens from + unauthorized access, disclosure, or use. This includes, but is not limited to: + + + Securely storing tokens + Transmitting them only over encrypted connections (HTTPS) + + Following the principle of least privilege when assigning token permissions + + Promptly revoking any compromised tokens + + Not hardcoding tokens in client-side code or public repositories + + + Regularly rotating tokens when used in production environments + + + + + 3.3 User Accounts + + + If you create a user account on the Core Platform, you are responsible for + maintaining the security of your account, and you are fully responsible for all + activities that occur under the account. You must immediately notify ACM @ UIUC of + any unauthorized uses of your account or any other breaches of security. + + + + 3.4 Rate Limiting + + + ACM @ UIUC reserves the right to set and enforce limits on your use of the Core + Platform in terms of the number of API requests that may be made and the number of + users you may serve. ACM @ UIUC may change these limits at any time, with or without + notice. Rate limits are designed to ensure fair usage of resources and may vary + based on the type of account or membership status. + + + + + + + + + 4. User Data and Privacy + + + + 4.1 Data Collection and Use + + If your application collects or processes user data: + + + You must maintain a privacy policy that clearly and accurately describes what user + data you collect and how you use and share such data with ACM @ UIUC and third + parties + + + You must obtain all necessary consents and provide all necessary disclosures + before collecting user data + + + You must comply with all applicable privacy and data protection laws and + regulations + + + You may not use any data accessed or obtained through the Core Platform for + advertising or marketing purposes + + + + + 4.2 Data Security + + + You must implement and maintain appropriate technical, physical, and administrative + safeguards to protect user data from unauthorized access, use, or disclosure. You + must promptly report any security breaches to ACM @ UIUC. + + + + 4.3 User Control and Transparency + + Your application must provide users with clear means to: + + View what data your application has access to + Revoke your application's access to their data + + Request deletion of their data that you have obtained through the Core Platform + + + + + 4.4 Data Retention + + + You will only retain user data for as long as necessary to provide your + application's functionality. If a user uninstalls your application, revokes + authorization, or requests data deletion, you must promptly delete all of their data + obtained through the Core Platform. + + + + + + + + + 5. Branding and Publicity + + + + 5.1 Attribution + + + When displaying data or content obtained through the Core Platform, you must + attribute ACM @ UIUC as the source by including a statement that clearly states the + information was accessed through the ACM @ UIUC Core Platform. + + + + 5.2 Use of Names and Logos + + + You may not use the names, logos, or trademarks of ACM @ UIUC, ACM, University of + Illinois, or any ACM @ UIUC affiliates without prior written permission, except as + expressly permitted in these Terms or other written agreement. + + + + + + + + + 6. Service Modifications and Availability + + + + 6.1 Modifications to the Platform + + + ACM @ UIUC may modify the Core Platform, including adding, removing, or changing + features or functionality, at any time and without liability to you. We will make + reasonable efforts to provide notice of material changes. + + + + 6.2 Monitoring and Quality Control + + + You agree that ACM @ UIUC may monitor use of the Core Platform to ensure quality, + improve the Core Platform, and verify your compliance with these Terms. This + monitoring may include accessing and using your application that utilizes our Core + Platform to identify security issues or compliance concerns. + + + + 6.3 Availability and Support + + + The Core Platform is provided on an "as is" and "as available" basis. ACM @ UIUC + does not guarantee that the Core Platform will be available at all times or that it + will be error-free. ACM @ UIUC does not provide any service level agreements or + warranties regarding Core Platform availability or performance. + + + + 6.4 Beta Features + + + ACM @ UIUC may make available certain Core Platform features on a beta or preview + basis. These features may be subject to additional terms and may not be as reliable + as other features. + + + + + + + + + 7. Term and Termination + + + + 7.1 Term + + These Terms will remain in effect until terminated by you or ACM @ UIUC. + + + 7.2 Termination by You + + + You may terminate these Terms at any time by ceasing all use of the Core Platform + and destroying all API keys and related materials. + + + + 7.3 Termination by ACM @ UIUC + + + ACM @ UIUC may terminate these Terms or suspend or revoke your access to the Core + Platform at any time for any reason without liability to you. Reasons for + termination may include, but are not limited to: + + + Violation of these Terms + + ACM @ UIUC determines that your use of the Core Platform poses a security risk or + could harm other users + + ACM @ UIUC is required to do so by law + ACM @ UIUC is no longer providing the Core Platform + + + + 7.4 Effect of Termination + + + Upon termination, all licenses granted herein immediately expire, and you must cease + all use of the Core Platform. Sections 4, 8, 9, 10, and 11 will survive termination. + + + + + + 8. Disclaimers + + + THE CORE PLATFORM IS PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTIES OF ANY + KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, IMPLIED WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. ACM @ + UIUC DOES NOT WARRANT THAT THE CORE PLATFORM WILL BE UNINTERRUPTED OR ERROR-FREE, OR + THAT DEFECTS WILL BE CORRECTED. + + + + + + 9. Limitation of Liability + + + TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL ACM @ UIUC, THE + UNIVERSITY OF ILLINOIS, THEIR OFFICERS, DIRECTORS, EMPLOYEES, VOLUNTEERS, OR AGENTS + BE LIABLE FOR ANY INDIRECT, PUNITIVE, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR + EXEMPLARY DAMAGES, INCLUDING WITHOUT LIMITATION DAMAGES FOR LOSS OF PROFITS, + GOODWILL, USE, DATA, OR OTHER INTANGIBLE LOSSES, THAT RESULT FROM THE USE OF, OR + INABILITY TO USE, THE CORE PLATFORM. + + + + SINCE THE CORE PLATFORM IS PROVIDED FREE OF CHARGE, ACM @ UIUC'S MAXIMUM AGGREGATE + LIABILITY TO YOU FOR ANY CLAIMS ARISING OUT OF OR RELATING TO THESE TERMS OR YOUR + USE OF THE CORE PLATFORM WILL NOT EXCEED $0 USD. IN THE EVENT YOU HAVE PAID FOR ANY + PREMIUM SERVICES RELATED TO THE CORE PLATFORM, ACM @ UIUC'S MAXIMUM LIABILITY WILL + BE LIMITED TO THE AMOUNT YOU ACTUALLY PAID TO ACM @ UIUC FOR SUCH SERVICES. + + + + + + 10. Indemnification + + + You agree to indemnify, defend, and hold harmless ACM @ UIUC, its officers, + directors, employees, volunteers, and agents from and against all claims, + liabilities, damages, losses, costs, expenses, and fees (including reasonable + attorneys' fees) that arise from or relate to your use of the Core Platform or + violation of these Terms. + + + + + + 11. General Legal Terms + + + + 11.1 Governing Law + + + These Terms shall be governed by and construed in accordance with the laws of the + State of Illinois, without regard to its conflict of law provisions. Any legal + action or proceeding relating to these Terms shall be brought exclusively in the + state or federal courts located in Champaign County, Illinois. + + + + 11.2 Amendments + + + ACM @ UIUC may amend these Terms at any time by posting the amended terms on the ACM + @ UIUC website or by providing notice to you. Your continued use of the Core + Platform after such posting or notification constitutes your acceptance of the + amended terms. + + + + 11.3 Assignment + + + You may not assign these Terms or any of your rights or obligations hereunder + without ACM @ UIUC's prior written consent. ACM @ UIUC may assign these Terms + without your consent. + + + + 11.4 Relationship to Open Source License + + + These Terms govern your use of the Core Platform service. Your use of the ACM @ UIUC + Core Platform source code is governed by the BSD 3-Clause License. In the event of a + conflict between these Terms and the BSD 3-Clause License with respect to the source + code, the BSD 3-Clause License shall prevail. + + + + 11.5 Entire Agreement + + + These Terms constitute the entire agreement between you and ACM @ UIUC regarding the + Core Platform and supersede all prior and contemporaneous agreements, proposals, or + representations, written or oral, concerning the subject matter of these Terms. + + + + 11.6 Severability + + + If any provision of these Terms is held to be invalid or unenforceable, such + provision shall be struck and the remaining provisions shall be enforced to the + fullest extent under law. + + + + 11.7 Waiver + + + The failure of ACM @ UIUC to enforce any right or provision of these Terms will not + be deemed a waiver of such right or provision. + + + + 11.8 Contributions + + + If you contribute to the ACM @ UIUC Core Platform source code, your contributions + will be licensed under the same BSD 3-Clause License that covers the project. You + represent that you have the legal right to provide any contributions you make. + + + + + + 12. Changes to Terms + + + ACM @ UIUC may modify these Terms at any time by posting the modified terms on our + website. We will make reasonable efforts to notify you of material changes through + the ACM @ UIUC website or other appropriate communication channels. Your continued + use of the Core Platform after such posting or notification constitutes your + acceptance of the modified terms. + + + If you do not agree to the modified Terms, you must stop using the Core Platform. + Changes will not apply retroactively and will become effective no sooner than 30 + days after they are posted, except for changes addressing new functions or changes + made for legal reasons, which will be effective immediately. + + + + + + 13. Contact Information + + If you have any questions about these Terms, please contact: + + ACM @ UIUC + 201 N Goodwin Avenue, Room 1104 + Urbana, IL 61801 + + Email:{' '} + officers@acm.illinois.edu + + + Website: acm.illinois.edu + + + + By using the ACM @ UIUC Core Platform, you acknowledge that you have read these + Terms, understand them, and agree to be bound by them. + + +
+
+
+ + ); +}; + +export default TermsOfService; diff --git a/src/ui/types.d.ts b/src/ui/types.d.ts new file mode 100644 index 00000000..fac6d444 --- /dev/null +++ b/src/ui/types.d.ts @@ -0,0 +1,25 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { type FastifyRequest, type FastifyInstance, type FastifyReply } from 'fastify'; +import { type AppRoles, type RunEnvironment } from '@common/roles.js'; +import type NodeCache from 'node-cache'; +import { type DynamoDBClient } from '@aws-sdk/client-dynamodb'; +import { type SecretsManagerClient } from '@aws-sdk/client-secrets-manager'; +import { type SQSClient } from '@aws-sdk/client-sqs'; +import { type CloudFrontKeyValueStoreClient } from '@aws-sdk/client-cloudfront-keyvaluestore'; +import { type AvailableAuthorizationPolicy } from '@common/policies/definition.js'; + +declare module 'fastify' { + interface FastifyRequest { + startTime: number; + username?: string; + userRoles?: Set; + tokenPayload?: AadToken; + policyRestrictions?: AvailableAuthorizationPolicy[]; + } +} + +export type NoDataRequest = { + Params: undefined; + Querystring: undefined; + Body: undefined; +}; diff --git a/src/ui/util/api.ts b/src/ui/util/api.ts index ec11f20d..f1ed2fea 100644 --- a/src/ui/util/api.ts +++ b/src/ui/util/api.ts @@ -1,13 +1,16 @@ -// src/api/index.js import axios from 'axios'; import { useMemo } from 'react'; import { useAuth } from '@ui/components/AuthContext'; import { getRunEnvironmentConfig, ValidService } from '@ui/config'; +export const MAX_API_TIMEOUT_MS = 5000; + const createAxiosInstance = (baseURL: string) => axios.create({ baseURL, + timeout: MAX_API_TIMEOUT_MS, + timeoutErrorMessage: 'The request timed out.', }); const useApi = (serviceName: ValidService) => { diff --git a/src/ui/vitest.setup.mjs b/src/ui/vitest.setup.mjs index 893396ea..2e5c7f59 100644 --- a/src/ui/vitest.setup.mjs +++ b/src/ui/vitest.setup.mjs @@ -1,3 +1,4 @@ +import "zod-openapi/extend" import '@testing-library/jest-dom/vitest'; import { vi } from 'vitest'; diff --git a/tests/live/apiKey.test.ts b/tests/live/apiKey.test.ts new file mode 100644 index 00000000..6b5dbe4c --- /dev/null +++ b/tests/live/apiKey.test.ts @@ -0,0 +1,16 @@ +import { expect, test, describe } from "vitest"; + +const baseEndpoint = `https://core.aws.qa.acmuiuc.org`; + +describe("API Key tests", async () => { + test("Test that auth is present on routes", { timeout: 10000 }, async () => { + const response = await fetch(`${baseEndpoint}/api/v1/apiKey/org`, { + method: "GET", + }); + expect(response.status).toBe(403); + const responsePost = await fetch(`${baseEndpoint}/api/v1/apiKey/org`, { + method: "POST", + }); + expect(responsePost.status).toBe(403); + }); +}); diff --git a/tests/live/documentation.test.ts b/tests/live/documentation.test.ts new file mode 100644 index 00000000..090dd9d4 --- /dev/null +++ b/tests/live/documentation.test.ts @@ -0,0 +1,19 @@ +import { expect, test } from "vitest"; + +const baseEndpoint = `https://core.aws.qa.acmuiuc.org`; + +test("Get OpenAPI JSON", async () => { + const response = await fetch(`${baseEndpoint}/api/documentation/json`); + expect(response.status).toBe(200); + + const responseDataJson = await response.json(); + expect(responseDataJson).toHaveProperty("openapi"); + expect(responseDataJson["openapi"]).toEqual("3.1.0"); +}); + +test("Get OpenAPI UI", async () => { + const response = await fetch(`${baseEndpoint}/api/documentation`); + expect(response.status).toBe(200); + const contentType = response.headers.get("content-type"); + expect(contentType).toContain("text/html"); +}); diff --git a/tests/live/events.test.ts b/tests/live/events.test.ts index 8a1e0d78..86ca1cf7 100644 --- a/tests/live/events.test.ts +++ b/tests/live/events.test.ts @@ -64,7 +64,7 @@ describe("Event lifecycle tests", async () => { }, }, ); - expect(response.status).toBe(201); + expect(response.status).toBe(204); }); test("check that deleted events cannot be found", async () => { diff --git a/tests/live/iam.test.ts b/tests/live/iam.test.ts new file mode 100644 index 00000000..4c6c804e --- /dev/null +++ b/tests/live/iam.test.ts @@ -0,0 +1,66 @@ +import { expect, test } from "vitest"; +import { createJwt } from "./utils.js"; +import { + EntraActionResponse, + GroupMemberGetResponse, +} from "../../src/common/types/iam.js"; +import { allAppRoles, AppRoles } from "../../src/common/roles.js"; + +const baseEndpoint = `https://core.aws.qa.acmuiuc.org`; +test("getting members of a group", async () => { + const token = await createJwt(); + const response = await fetch( + `${baseEndpoint}/api/v1/iam/groups/dbe18eb2-9675-46c4-b1ef-749a6db4fedd`, + { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }, + ); + expect(response.status).toBe(200); + const responseJson = (await response.json()) as GroupMemberGetResponse; + expect(responseJson.length).greaterThan(0); + for (const item of responseJson) { + expect(item).toHaveProperty("name"); + expect(item).toHaveProperty("email"); + expect(item["name"].length).greaterThan(0); + expect(item["email"].length).greaterThan(0); + expect(item["email"]).toContain("@"); + } +}); + +test("inviting users to tenant", { timeout: 60000 }, async () => { + const token = await createJwt(); + const response = await fetch(`${baseEndpoint}/api/v1/iam/inviteUsers`, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + emails: ["acm@illinois.edu"], + }), + }); + expect(response.status).toBe(202); + const responseJson = (await response.json()) as EntraActionResponse; + expect(responseJson).toEqual({ + success: [{ email: "acm@illinois.edu" }], + failure: [], + }); +}); + +test("getting group roles", async () => { + const token = await createJwt(); + const response = await fetch(`${baseEndpoint}/api/v1/iam/groups/0/roles`, { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }); + expect(response.status).toBe(200); + const responseJson = (await response.json()) as AppRoles[]; + expect(responseJson).toEqual(allAppRoles); +}); diff --git a/tests/live/protected.test.ts b/tests/live/protected.test.ts index 12a96ccd..63fd30ca 100644 --- a/tests/live/protected.test.ts +++ b/tests/live/protected.test.ts @@ -10,7 +10,7 @@ describe("Role checking live API tests", async () => { "Test that auth is present on the GET route", { timeout: 10000 }, async () => { - const response = await fetch(`${baseEndpoint}/api/v1/protected/`, { + const response = await fetch(`${baseEndpoint}/api/v1/protected`, { method: "GET", }); expect(response.status).toBe(403); diff --git a/tests/tsconfig.json b/tests/tsconfig.json index 343bcab1..b2b11cbf 100644 --- a/tests/tsconfig.json +++ b/tests/tsconfig.json @@ -2,7 +2,6 @@ "compilerOptions": { "target": "ES2020", "module": "NodeNext", - "moduleResolution": "node", "esModuleInterop": true, "strict": true, "skipLibCheck": true, diff --git a/tests/unit/apiKey.test.ts b/tests/unit/apiKey.test.ts new file mode 100644 index 00000000..57ffe02c --- /dev/null +++ b/tests/unit/apiKey.test.ts @@ -0,0 +1,350 @@ +import { afterAll, expect, test, beforeEach, vi, describe } from "vitest"; +import { mockClient } from "aws-sdk-client-mock"; +import init from "../../src/api/index.js"; +import { createJwt } from "./auth.test.js"; +import { secretJson, secretObject } from "./secret.testdata.js"; +import supertest from "supertest"; +import { + GetSecretValueCommand, + SecretsManagerClient, +} from "@aws-sdk/client-secrets-manager"; +import { + ConditionalCheckFailedException, + DynamoDBClient, + ScanCommand, + TransactWriteItemsCommand, +} from "@aws-sdk/client-dynamodb"; +import { AppRoles } from "../../src/common/roles.js"; +import { createApiKey } from "../../src/api/functions/apiKey.js"; + +// Mock the createApiKey function +vi.mock("../../src/api/functions/apiKey.js", () => { + return { + createApiKey: vi.fn().mockImplementation(async () => { + return { + apiKey: "acmuiuc_test123_abcdefg12345", + hashedKey: "hashed_key_value", + keyId: "test123", + }; + }), + }; +}); + +// Mock DynamoDB client +const dynamoMock = mockClient(DynamoDBClient); +const smMock = mockClient(SecretsManagerClient); +const jwt_secret = secretObject["jwt_key"]; + +vi.stubEnv("JwtSigningKey", jwt_secret); + +const app = await init(); + +describe("API Key Route Tests", () => { + beforeEach(() => { + dynamoMock.reset(); + smMock.reset(); + vi.clearAllMocks(); + + smMock.on(GetSecretValueCommand).resolves({ + SecretString: secretJson, + }); + + dynamoMock.on(TransactWriteItemsCommand).resolves({}); + + dynamoMock.on(ScanCommand).resolves({ + Items: [ + { + keyId: { S: "test123" }, + roles: { L: [{ S: AppRoles.EVENTS_MANAGER }] }, + description: { S: "Test API Key" }, + owner: { S: "testuser" }, + createdAt: { N: "1618012800" }, + keyHash: { S: "hashed_key_value" }, + }, + ], + }); + }); + + afterAll(async () => { + await app.close(); + }); + + describe("Create API Key", () => { + test("Should create an API key successfully", async () => { + const testJwt = createJwt(); + await app.ready(); + + // Make the request + const response = await supertest(app.server) + .post("/api/v1/apiKey/org") + .set("authorization", `Bearer ${testJwt}`) + .send({ + roles: [AppRoles.EVENTS_MANAGER], + description: "Test API Key", + }); + + // Assertions + expect(response.statusCode).toBe(201); + expect(response.body).toHaveProperty("apiKey"); + expect(response.body.apiKey).toBe("acmuiuc_test123_abcdefg12345"); + expect(createApiKey).toHaveBeenCalledTimes(1); + expect(dynamoMock.calls()).toHaveLength(1); + }); + + test("Should create an API key with expiration", async () => { + const testJwt = createJwt(); + await app.ready(); + + const expiryTime = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now + + // Make the request + const response = await supertest(app.server) + .post("/api/v1/apiKey/org") + .set("authorization", `Bearer ${testJwt}`) + .send({ + roles: [AppRoles.EVENTS_MANAGER], + description: "Test API Key with Expiry", + expiresAt: expiryTime, + }); + + // Assertions + expect(response.statusCode).toBe(201); + expect(response.body).toHaveProperty("apiKey"); + expect(response.body).toHaveProperty("expiresAt"); + expect(response.body.expiresAt).toBe(expiryTime); + expect(createApiKey).toHaveBeenCalledTimes(1); + expect(dynamoMock.calls()).toHaveLength(1); + }); + + test("Should not create an API key for invalid API key roles", async () => { + const testJwt = createJwt(); + await app.ready(); + + const expiryTime = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now + + // Make the request + const response = await supertest(app.server) + .post("/api/v1/apiKey/org") + .set("authorization", `Bearer ${testJwt}`) + .send({ + roles: [AppRoles.MANAGE_ORG_API_KEYS], + description: "Test bad API key", + expiresAt: expiryTime, + }); + + // Assertions + expect(response.statusCode).toBe(400); + expect(response.body).toHaveProperty("error"); + expect(response.body.error).toEqual(true); + expect(response.body.name).toEqual("ValidationError"); + expect(response.body.id).toEqual(104); + + expect(createApiKey).toHaveBeenCalledTimes(0); + expect(dynamoMock.calls()).toHaveLength(0); + }); + + test("Should handle DynamoDB insertion error", async () => { + // Mock the DynamoDB client to throw an error + dynamoMock + .on(TransactWriteItemsCommand) + .rejects(new Error("DynamoDB error")); + + const testJwt = createJwt(); + await app.ready(); + + // Make the request + const response = await supertest(app.server) + .post("/api/v1/apiKey/org") + .set("authorization", `Bearer ${testJwt}`) + .send({ + roles: [AppRoles.EVENTS_MANAGER], + description: "Test API Key", + }); + + // Assertions + expect(response.statusCode).toBe(500); + expect(response.body).toHaveProperty("message"); + expect(response.body.message).toBe("Could not create API key."); + expect(createApiKey).toHaveBeenCalledTimes(1); + expect(dynamoMock.calls()).toHaveLength(1); + }); + + test("Should require authorization", async () => { + await app.ready(); + + // Make the request without a JWT + const response = await supertest(app.server) + .post("/api/v1/apiKey/org") + .send({ + roles: [AppRoles.EVENTS_MANAGER], + description: "Test API Key", + }); + + // Assertions + expect(response.statusCode).toBe(403); + }); + }); + + describe("Delete API Key", () => { + test("Should delete an API key successfully", async () => { + const testJwt = createJwt(); + await app.ready(); + + // Make the request + const response = await supertest(app.server) + .delete("/api/v1/apiKey/org/test123") + .set("authorization", `Bearer ${testJwt}`); + + // Assertions + expect(response.statusCode).toBe(204); + expect(dynamoMock.calls()).toHaveLength(1); + }); + + test("Should handle key not found error", async () => { + // Mock the DynamoDB client to throw ConditionalCheckFailedException + dynamoMock.on(TransactWriteItemsCommand).rejects( + new ConditionalCheckFailedException({ + $metadata: {}, + message: "The conditional request failed", + }), + ); + + const testJwt = createJwt(); + await app.ready(); + + // Make the request + const response = await supertest(app.server) + .delete("/api/v1/apiKey/org/nonexistent") + .set("authorization", `Bearer ${testJwt}`); + + // Assertions + expect(response.statusCode).toBe(400); + expect(response.body).toHaveProperty("message"); + expect(response.body.message).toBe("Key does not exist."); + expect(dynamoMock.calls()).toHaveLength(1); + }); + + test("Should handle DynamoDB deletion error", async () => { + // Mock the DynamoDB client to throw a generic error + dynamoMock + .on(TransactWriteItemsCommand) + .rejects(new Error("DynamoDB error")); + + const testJwt = createJwt(); + await app.ready(); + + // Make the request + const response = await supertest(app.server) + .delete("/api/v1/apiKey/org/test123") + .set("authorization", `Bearer ${testJwt}`); + + // Assertions + expect(response.statusCode).toBe(500); + expect(response.body).toHaveProperty("message"); + expect(response.body.message).toBe("Could not delete API key."); + expect(dynamoMock.calls()).toHaveLength(1); + }); + + test("Should require authentication", async () => { + await app.ready(); + + // Make the request without a JWT + const response = await supertest(app.server).delete( + "/api/v1/apiKey/org/test123", + ); + + // Assertions + expect(response.statusCode).toBe(403); + }); + }); + + describe("GET /org - Get All API Keys", () => { + test("Should get all API keys successfully", async () => { + const testJwt = createJwt(); + await app.ready(); + + // Make the request + const response = await supertest(app.server) + .get("/api/v1/apiKey/org") + .set("authorization", `Bearer ${testJwt}`); + + // Assertions + expect(response.statusCode).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + expect(response.body.length).toBe(1); + expect(response.body[0]).toHaveProperty("keyId", "test123"); + expect(response.body[0]).toHaveProperty("description", "Test API Key"); + expect(response.body[0]).not.toHaveProperty("keyHash"); // keyHash should be filtered out + expect(dynamoMock.calls()).toHaveLength(1); + }); + + test("Should handle empty result", async () => { + // Mock the DynamoDB client to return empty results + dynamoMock.on(ScanCommand).resolves({ + Items: [], + }); + + const testJwt = createJwt(); + await app.ready(); + + // Make the request + const response = await supertest(app.server) + .get("/api/v1/apiKey/org") + .set("authorization", `Bearer ${testJwt}`); + + // Assertions + expect(response.statusCode).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + expect(response.body.length).toBe(0); + expect(dynamoMock.calls()).toHaveLength(1); + }); + + test("Should handle missing Items in response", async () => { + // Mock the DynamoDB client to return a response without Items + dynamoMock.on(ScanCommand).resolves({}); + + const testJwt = createJwt(); + await app.ready(); + + // Make the request + const response = await supertest(app.server) + .get("/api/v1/apiKey/org") + .set("authorization", `Bearer ${testJwt}`); + + // Assertions + expect(response.statusCode).toBe(500); + expect(response.body).toHaveProperty("message"); + expect(response.body.message).toBe("Could not fetch API keys."); + expect(dynamoMock.calls()).toHaveLength(1); + }); + + test("Should handle DynamoDB fetch error", async () => { + // Mock the DynamoDB client to throw an error + dynamoMock.on(ScanCommand).rejects(new Error("DynamoDB error")); + + const testJwt = createJwt(); + await app.ready(); + + // Make the request + const response = await supertest(app.server) + .get("/api/v1/apiKey/org") + .set("authorization", `Bearer ${testJwt}`); + + // Assertions + expect(response.statusCode).toBe(500); + expect(response.body).toHaveProperty("message"); + expect(response.body.message).toBe("Could not fetch API keys."); + expect(dynamoMock.calls()).toHaveLength(1); + }); + + test("Should require authentication", async () => { + await app.ready(); + + // Make the request without a JWT + const response = await supertest(app.server).get("/api/v1/apiKey/org"); + + // Assertions + expect(response.statusCode).toBe(403); + }); + }); +}); diff --git a/tests/unit/auth.test.ts b/tests/unit/auth.test.ts index f69257fa..ed4bfb9a 100644 --- a/tests/unit/auth.test.ts +++ b/tests/unit/auth.test.ts @@ -19,7 +19,7 @@ const ddbMock = mockClient(SecretsManagerClient); const app = await init(); const jwt_secret = secretObject["jwt_key"]; -export function createJwt(date?: Date, group?: string, email?: string) { +export function createJwt(date?: Date, groups?: string[], email?: string) { let modifiedPayload = { ...jwtPayload, email: email || jwtPayload.email, @@ -36,8 +36,8 @@ export function createJwt(date?: Date, group?: string, email?: string) { }; } - if (group) { - modifiedPayload.groups[0] = group; + if (groups) { + modifiedPayload.groups = groups; } return jwt.sign(modifiedPayload, jwt_secret, { algorithm: "HS256" }); } diff --git a/tests/unit/documentation.test.ts b/tests/unit/documentation.test.ts new file mode 100644 index 00000000..94962fbf --- /dev/null +++ b/tests/unit/documentation.test.ts @@ -0,0 +1,30 @@ +import { afterAll, expect, test } from "vitest"; +import init from "../../src/api/index.js"; + +const app = await init(); +test("Test getting OpenAPI JSON", async () => { + const response = await app.inject({ + method: "GET", + url: "/api/documentation/json", + }); + expect(response.statusCode).toBe(200); + const responseDataJson = await response.json(); + expect(responseDataJson).toHaveProperty("openapi"); + expect(responseDataJson["openapi"]).toEqual("3.1.0"); +}); +afterAll(async () => { + await app.close(); +}); + +test("Test getting OpenAPI UI", async () => { + const response = await app.inject({ + method: "GET", + url: "/api/documentation", + }); + expect(response.statusCode).toBe(200); + const contentType = response.headers["content-type"]; + expect(contentType).toContain("text/html"); +}); +afterAll(async () => { + await app.close(); +}); diff --git a/tests/unit/eventPost.test.ts b/tests/unit/eventPost.test.ts index af1d9785..40400d6a 100644 --- a/tests/unit/eventPost.test.ts +++ b/tests/unit/eventPost.test.ts @@ -48,7 +48,7 @@ test("Sad path: Not authenticated", async () => { test("Sad path: Authenticated but not authorized", async () => { await app.ready(); - const testJwt = createJwt(undefined, "1"); + const testJwt = createJwt(undefined, ["1"]); const response = await supertest(app.server) .post("/api/v1/events") .set("Authorization", `Bearer ${testJwt}`) @@ -66,7 +66,7 @@ test("Sad path: Authenticated but not authorized", async () => { }); test("Sad path: Prevent empty body request", async () => { await app.ready(); - const testJwt = createJwt(undefined, "0"); + const testJwt = createJwt(undefined, ["0"]); const response = await supertest(app.server) .post("/api/v1/events") .set("Authorization", `Bearer ${testJwt}`) @@ -76,7 +76,7 @@ test("Sad path: Prevent empty body request", async () => { error: true, name: "ValidationError", id: 104, - message: `Required at "title"; Required at "description"; Required at "start"; Required at "location"; Required at "host"`, + message: "body/ Expected object, received null", }); }); test("Sad path: Prevent specifying repeatEnds on non-repeating events", async () => { @@ -106,7 +106,7 @@ test("Sad path: Prevent specifying repeatEnds on non-repeating events", async () error: true, name: "ValidationError", id: 104, - message: "repeats is required when repeatEnds is defined", + message: "body/ repeats is required when repeatEnds is defined", }); }); @@ -137,7 +137,8 @@ test("Sad path: Prevent specifying unknown repeat frequencies", async () => { error: true, name: "ValidationError", id: 104, - message: `Invalid enum value. Expected 'weekly' | 'biweekly', received 'forever_and_ever' at "repeats"`, + message: + "body/repeats Invalid enum value. Expected 'weekly' | 'biweekly', received 'forever_and_ever'", }); }); @@ -226,7 +227,7 @@ describe("ETag Lifecycle Tests", () => { Items: [], }); - const testJwt = createJwt(undefined, "0"); + const testJwt = createJwt(undefined, ["0"]); // 1. Check initial etag for all events is 0 const initialAllResponse = await app.inject({ @@ -312,7 +313,7 @@ describe("ETag Lifecycle Tests", () => { Items: [], }); - const testJwt = createJwt(undefined, "0"); + const testJwt = createJwt(undefined, ["0"]); // 1. Create an event const eventResponse = await supertest(app.server) @@ -360,7 +361,7 @@ describe("ETag Lifecycle Tests", () => { .delete(`/api/v1/events/${eventId}`) .set("Authorization", `Bearer ${testJwt}`); - expect(deleteResponse.statusCode).toBe(201); + expect(deleteResponse.statusCode).toBe(204); // 4. Verify the event no longer exists (should return 404) // Change the mock to return empty response (simulating deleted event) @@ -412,7 +413,7 @@ describe("ETag Lifecycle Tests", () => { Items: [], }); - const testJwt = createJwt(undefined, "0"); + const testJwt = createJwt(undefined, ["0"]); // 1. Check initial etag for all events is 0 const initialAllResponse = await app.inject({ diff --git a/tests/unit/events.test.ts b/tests/unit/events.test.ts index 75f5256f..b6478a8b 100644 --- a/tests/unit/events.test.ts +++ b/tests/unit/events.test.ts @@ -50,7 +50,7 @@ test("ETag should increment after event creation", async () => { Items: [], }); - const testJwt = createJwt(undefined, "0"); + const testJwt = createJwt(undefined, ["0"]); // 1. Check initial etag for all events is 0 const initialAllResponse = await app.inject({ @@ -138,7 +138,7 @@ test("Should return 304 Not Modified when If-None-Match header matches ETag", as Items: [], }); - const testJwt = createJwt(undefined, "0"); + const testJwt = createJwt(undefined, ["0"]); // 1. First GET request to establish ETag const initialResponse = await app.inject({ @@ -188,7 +188,7 @@ test("Should return 304 Not Modified when If-None-Match header matches quoted ET Items: [], }); - const testJwt = createJwt(undefined, "0"); + const testJwt = createJwt(undefined, ["0"]); // 1. First GET request to establish ETag const initialResponse = await app.inject({ @@ -238,7 +238,7 @@ test("Should NOT return 304 when ETag has changed", async () => { Items: [], }); - const testJwt = createJwt(undefined, "0"); + const testJwt = createJwt(undefined, ["0"]); // 1. Initial GET to establish ETag const initialResponse = await app.inject({ @@ -313,7 +313,7 @@ test("Should handle 304 responses for individual event endpoints", async () => { ddbMock.on(PutItemCommand).resolves({}); // Create an event - const testJwt = createJwt(undefined, "0"); + const testJwt = createJwt(undefined, ["0"]); const eventResponse = await supertest(app.server) .post("/api/v1/events") .set("Authorization", `Bearer ${testJwt}`) diff --git a/tests/unit/functions/apiKey.test.ts b/tests/unit/functions/apiKey.test.ts new file mode 100644 index 00000000..33e42583 --- /dev/null +++ b/tests/unit/functions/apiKey.test.ts @@ -0,0 +1,68 @@ +import * as argon2 from "argon2"; +import { expect, test, describe, vi } from "vitest"; +import { API_KEY_CACHE_SECONDS, createApiKey, getApiKeyData, getApiKeyParts, verifyApiKey } from "../../../src/api/functions/apiKey.js"; +import { mockClient } from "aws-sdk-client-mock"; +import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb"; +import { genericConfig } from "../../../src/common/config.js"; +import { allAppRoles } from "../../../src/common/roles.js"; +import NodeCache from "node-cache"; +import { unmarshall } from "@aws-sdk/util-dynamodb"; + + + +const ddbMock = mockClient(DynamoDBClient); + + +const countOccurrencesOfChar = (s: string, char: string): number => { + let count = 0; + for (const item of s) { + if (item == char) { + count++; + } + } + return count; +} + +describe("Audit Log tests", () => { + test("API key is successfully created and validated", async () => { + const { apiKey, hashedKey, keyId } = await createApiKey(); + expect(apiKey.slice(0, 8)).toEqual("acmuiuc_"); + expect(keyId.length).toEqual(12); + expect(countOccurrencesOfChar(apiKey, "_")).toEqual(3); + const verificationResult = await verifyApiKey({ apiKey, hashedKey }); + expect(verificationResult).toBe(true); + }); + test("API Keys that don't start with correct prefix are rejected", async () => { + const { apiKey, hashedKey } = await createApiKey(); + const verificationResult = await verifyApiKey({ apiKey: apiKey.replace("acmuiuc_", "acm_"), hashedKey: hashedKey }); + expect(verificationResult).toBe(false); + }); + test("API Keys that have an incorrect checksum are rejected", async () => { + const { apiKey, hashedKey } = await createApiKey(); + const submittedChecksum = apiKey.split("_")[3]; + const verificationResult = await verifyApiKey({ apiKey: apiKey.replace(submittedChecksum, "123456"), hashedKey: hashedKey }); + expect(verificationResult).toBe(false); + }); + test("Retrieving API keys from DynamoDB works correctly and is cached", async () => { + const { apiKey, hashedKey } = await createApiKey(); + const { prefix, id, rawKey, checksum } = getApiKeyParts(apiKey); + const keyData = { + keyId: { S: id }, + keyHash: { S: hashedKey }, + roles: { SS: allAppRoles } + } + ddbMock.on(GetItemCommand, { + TableName: genericConfig.ApiKeyTable, + Key: { "keyId": { S: id } } + }).resolves({ + Item: keyData + }) + const nodeCache = new NodeCache(); + const dynamoClient = new DynamoDBClient() + const now = Date.now() / 1000; + const result = await getApiKeyData({ nodeCache, dynamoClient, id }); + expect(result).toEqual(unmarshall(keyData)); + expect(nodeCache.get(`auth_apikey_${id}`)).toEqual(unmarshall(keyData)); + expect(nodeCache.getTtl(`auth_apikey_${id}`)).toBeGreaterThan(now); + }) +}); diff --git a/tests/unit/functions/auditLog.test.ts b/tests/unit/functions/auditLog.test.ts new file mode 100644 index 00000000..c79da925 --- /dev/null +++ b/tests/unit/functions/auditLog.test.ts @@ -0,0 +1,158 @@ +import { expect, test, describe, vi } from "vitest"; +import { createAuditLogEntry, buildAuditLogTransactPut } from "../../../src/api/functions/auditLog"; +import { mockClient } from "aws-sdk-client-mock"; +import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb"; +import { afterEach, beforeEach } from "node:test"; +import { genericConfig } from "../../../src/common/config"; +import { Modules } from "../../../src/common/modules.js"; +import { marshall } from "@aws-sdk/util-dynamodb"; + + +const ddbMock = mockClient(DynamoDBClient); + + +describe("Audit Log tests", () => { + test("Audit log entry with request ID is correctly added", async () => { + const mockDate = new Date(2025, 3, 20, 12, 0, 0); + const mockTimestamp = mockDate.getTime(); + vi.spyOn(Date, 'now').mockReturnValue(mockTimestamp); + const timestamp = Math.floor(mockTimestamp / 1000); + const expireAt = Math.floor( + (mockTimestamp + 365 * 24 * 60 * 60 * 1000) / 1000 + ); + + const payload = { + module: Modules.IAM, + actor: 'admin@acm.illinois.edu', + target: 'nonadmin@acm.illinois.edu', + requestId: 'abcdef', + message: "Created user" + }; + + const expectedItem = { + ...payload, + createdAt: timestamp, + expireAt: expireAt + }; + + ddbMock.on(PutItemCommand, { + TableName: genericConfig.AuditLogTable, + Item: marshall(expectedItem) + }).resolvesOnce({ ConsumedCapacity: { WriteCapacityUnits: 1 } }).rejects({ message: "Called more than once." }); + + const result = await createAuditLogEntry({ entry: payload }); + expect(result).toStrictEqual({ ConsumedCapacity: { WriteCapacityUnits: 1 } }); + }); +}); + +describe("Audit Log Transaction tests", () => { + test("Audit log transaction item is correctly created with all fields", () => { + // Setup mock date + const mockDate = new Date(2025, 3, 20, 12, 0, 0); + const mockTimestamp = mockDate.getTime(); + vi.spyOn(Date, 'now').mockReturnValue(mockTimestamp); + + const timestamp = Math.floor(mockTimestamp / 1000); + const expireAt = timestamp + Math.floor((365 * 24 * 60 * 60 * 1000) / 1000); + + // Create test payload + const payload = { + module: Modules.IAM, + actor: 'admin@acm.illinois.edu', + target: 'nonadmin@acm.illinois.edu', + requestId: 'abcdef', + message: "Created user" + }; + + // Expected marshalled item + const expectedItem = marshall({ + ...payload, + createdAt: timestamp, + expireAt: expireAt + }); + + // Call the function being tested + const transactItem = buildAuditLogTransactPut({ entry: payload }); + + // Verify the result + expect(transactItem).toStrictEqual({ + Put: { + TableName: genericConfig.AuditLogTable, + Item: expectedItem + } + }); + }); + + test("Audit log transaction item with minimal fields is correctly created", () => { + // Setup mock date + const mockDate = new Date(2025, 3, 20, 12, 0, 0); + const mockTimestamp = mockDate.getTime(); + vi.spyOn(Date, 'now').mockReturnValue(mockTimestamp); + + const timestamp = Math.floor(mockTimestamp / 1000); + const expireAt = timestamp + Math.floor((365 * 24 * 60 * 60 * 1000) / 1000); + + // Create test payload with only required fields + const payload = { + module: Modules.IAM, + actor: 'admin@acm.illinois.edu', + message: "Deleted resource" + }; + + // Expected marshalled item + const expectedItem = marshall({ + ...payload, + createdAt: timestamp, + expireAt: expireAt + }); + + // Call the function being tested + const transactItem = buildAuditLogTransactPut({ entry: payload }); + + // Verify the result + expect(transactItem).toStrictEqual({ + Put: { + TableName: genericConfig.AuditLogTable, + Item: expectedItem + } + }); + }); + + test("Audit log transaction item correctly calculates expiration timestamp", () => { + // Setup mock date + const mockDate = new Date(2025, 3, 20, 12, 0, 0); + const mockTimestamp = mockDate.getTime(); + vi.spyOn(Date, 'now').mockReturnValue(mockTimestamp); + + const timestamp = Math.floor(mockTimestamp / 1000); + // Manually calculate the expected expiration + const retentionDays = 365; + const secondsInDay = 24 * 60 * 60; + const millisecondsInDay = secondsInDay * 1000; + const expectedExpireAt = timestamp + Math.floor((retentionDays * millisecondsInDay) / 1000); + + // Create test payload + const payload = { + module: Modules.IAM, + actor: 'admin@acm.illinois.edu', + message: "Modified settings" + }; + + // Call the function being tested + const transactItem = buildAuditLogTransactPut({ entry: payload }); + + // Extract and verify the expiration timestamp + const marshalledItem = transactItem.Put.Item; + const unmarshalledItem = require('@aws-sdk/util-dynamodb').unmarshall(marshalledItem); + + expect(unmarshalledItem.expireAt).toBe(expectedExpireAt); + }); +}); + +beforeEach(() => { + ddbMock.reset(); +}); + +afterEach(() => { + vi.useRealTimers(); +}); diff --git a/tests/unit/ical.test.ts b/tests/unit/ical.test.ts index 10441a40..f26c75e3 100644 --- a/tests/unit/ical.test.ts +++ b/tests/unit/ical.test.ts @@ -33,9 +33,7 @@ test("Test getting ACM-wide iCal calendar", async () => { test("Test getting non-existent iCal calendar fails", async () => { const date = new Date(2024, 7, 22, 15, 51, 48); // August 22, 2024, at 15:51:48 (3:51:48 PM) vi.setSystemTime(date); - ddbMock.on(ScanCommand).resolves({ - Items: dynamoTableData as any, - }); + ddbMock.on(ScanCommand).rejects(); const response = await app.inject({ method: "GET", url: "/api/v1/ical/invalid", diff --git a/tests/unit/linkry.test.ts b/tests/unit/linkry.test.ts new file mode 100644 index 00000000..23b655d7 --- /dev/null +++ b/tests/unit/linkry.test.ts @@ -0,0 +1,604 @@ +import { beforeEach, expect, test, vi } from "vitest"; +import { + DynamoDBClient, + ScanCommand, + QueryCommand, + TransactWriteItemsCommand, + TransactionCanceledException, +} from "@aws-sdk/client-dynamodb"; +import { mockClient } from "aws-sdk-client-mock"; +import init from "../../src/api/index.js"; +import { createJwt } from "./auth.test.js"; +import { + GetSecretValueCommand, + SecretsManagerClient, +} from "@aws-sdk/client-secrets-manager"; + +import { secretJson, secretObject } from "./secret.testdata.js"; +import supertest from "supertest"; +import { dynamoTableData } from "./mockLinkryData.testdata.js"; +import { genericConfig } from "../../src/common/config.js"; + +const ddbMock = mockClient(DynamoDBClient); +const smMock = mockClient(SecretsManagerClient); +const jwt_secret = secretObject["jwt_key"]; +vi.stubEnv("JwtSigningKey", jwt_secret); + +// Mock the Cloudfront KV client to prevent the actual Cloudfront KV call +// aws-sdk-client-mock doesn't support Cloudfront KV Client API +vi.mock("../../src/api/functions/cloudfrontKvStore.js", async () => { + return { + setKey: vi.fn(), + deleteKey: vi.fn(), + getKey: vi.fn().mockResolvedValue("https://www.acm.illinois.edu/"), + getLinkryKvArn: vi + .fn() + .mockResolvedValue( + "arn:aws:cloudfront::1234567890:key-value-store/bb90421c-e923-4bd7-a42a-7281150389c3s", + ), + }; +}); + +const app = await init(); + +(app as any).nodeCache.flushAll(); +ddbMock.reset(); +smMock.reset(); +vi.useFakeTimers(); + +// Mock secrets manager +smMock.on(GetSecretValueCommand).resolves({ + SecretString: secretJson, +}); + +const adminJwt = createJwt(undefined, ["LINKS_ADMIN"], "test@gmail.com"); + +beforeEach(() => { + ddbMock.reset(); +}); +// Get Link +beforeEach(() => { + ddbMock.reset(); +}); +// Get Link + +test("Happy path: Fetch all linkry redirects with admin roles", async () => { + ddbMock + .on(QueryCommand, { TableName: genericConfig.LinkryDynamoTableName }) + .resolves({ + Items: [], + }); + + ddbMock + .on(ScanCommand, { TableName: genericConfig.LinkryDynamoTableName }) + .resolvesOnce({ + Items: dynamoTableData, + }) + .rejects(); + + const response = await app.inject({ + method: "GET", + url: "/api/v1/linkry/redir", + headers: { + Authorization: `Bearer ${adminJwt}`, + }, + }); + + expect(response.statusCode).toBe(200); + let body = JSON.parse(response.body); + expect(body.ownedLinks).toEqual([]); +}); + +test("Happy path: Fetch all linkry redirects with admin roles owned", async () => { + const adminOwnedJwt = createJwt( + undefined, + ["LINKS_ADMIN"], + "bob@illinois.edu", + ); + + ddbMock + .on(QueryCommand, { TableName: genericConfig.LinkryDynamoTableName }) + .resolves({ + Items: dynamoTableData, + }); + + ddbMock + .on(ScanCommand, { TableName: genericConfig.LinkryDynamoTableName }) + .resolvesOnce({ + Items: dynamoTableData, + }) + .rejects(); + + const response = await app.inject({ + method: "GET", + url: "/api/v1/linkry/redir", + headers: { + Authorization: `Bearer ${adminOwnedJwt}`, + }, + }); + expect(response.statusCode).toBe(200); + let body = JSON.parse(response.body); + expect(body.delegatedLinks).toEqual([]); +}); + +test("Make sure that a DB scan is only called for admins", async () => { + const testManagerJwt = createJwt( + undefined, + ["LINKS_MANAGER"], + "test@gmail.com", + ); + + ddbMock + .on(QueryCommand, { TableName: genericConfig.LinkryDynamoTableName }) + .resolves({ + Items: [], + }); + + ddbMock + .on(ScanCommand, { TableName: genericConfig.LinkryDynamoTableName }) + .rejects(); + + const response = await app.inject({ + method: "GET", + url: "/api/v1/linkry/redir", + headers: { + Authorization: `Bearer ${testManagerJwt}`, + }, + }); + + expect(response.statusCode).toBe(200); +}); + +test("Unhappy path: Fetch all linkry redirects database error", async () => { + ddbMock + .on(QueryCommand, { TableName: genericConfig.LinkryDynamoTableName }) + .rejects(); + + ddbMock + .on(ScanCommand, { TableName: genericConfig.LinkryDynamoTableName }) + .rejects(); + + const response = await app.inject({ + method: "GET", + url: "/api/v1/linkry/redir", + headers: { + Authorization: `Bearer ${adminJwt}`, + }, + }); + + expect(response.statusCode).toBe(500); + let body = JSON.parse(response.body); + expect(body.name).toEqual("DatabaseFetchError"); +}); + +//Create/Edit Link +test("Happy path: Create/Edit linkry redirect success", async () => { + const userJwt = createJwt( + undefined, + ["LINKS_MANAGER", "940e4f9e-6891-4e28-9e29-148798495cdb"], + "alice@illinois.edu", + ); + ddbMock + .on(QueryCommand, { TableName: genericConfig.LinkryDynamoTableName }) + .resolves({ + Items: dynamoTableData, + }); + + ddbMock.on(TransactWriteItemsCommand).resolves({}); + + const payload = { + access: ["940e4f9e-6891-4e28-9e29-148798495cdb", "newAccessGroupid1123"], + redirect: "https://www.acm.illinois.edu/", + slug: "WlQDmu", + }; + + const response = await supertest(app.server) + .post("/api/v1/linkry/redir") + .set("Authorization", `Bearer ${userJwt}`) + .send(payload); + + expect(response.statusCode).toBe(201); +}); + +test("Unhappy path: Edit linkry redirect not authorized", async () => { + const userJwt = createJwt( + undefined, + ["LINKS_MANAGER", "IncorrectGroupID233"], + "alice@illinois.edu", + ); + ddbMock + .on(QueryCommand, { TableName: genericConfig.LinkryDynamoTableName }) + .resolves({ + Items: dynamoTableData, + }); + + ddbMock.on(TransactWriteItemsCommand).resolves({}); + + const payload = { + access: [], + redirect: "https://www.acm.illinois.edu/", + slug: "WlQDmu", + }; + + const response = await supertest(app.server) + .post("/api/v1/linkry/redir") + .set("Authorization", `Bearer ${userJwt}`) + .send(payload); + + expect(response.statusCode).toBe(401); + expect(response.body.name).toEqual("UnauthorizedError"); +}); + +test("Unhappy path: Edit linkry time stamp mismatch", async () => { + const userJwt = createJwt(undefined, ["LINKS_ADMIN"], "alice@illinois.edu"); + ddbMock + .on(QueryCommand, { TableName: genericConfig.LinkryDynamoTableName }) + .resolves({ + Items: [ + ...dynamoTableData, + { + slug: { + S: "WlQDmu", + }, + access: { + S: "GROUP#940e4f9e-6891-4e28-9e29-148798495cdb", + }, + createdAt: { + S: "2030-04-18T18:36:50.706Z", + }, + updatedAt: { + S: "2030-04-18T18:37:40.681Z", + }, + }, + ], + }); + + ddbMock.on(TransactWriteItemsCommand).resolves({}); + + const payload = { + access: [], + redirect: "https://www.acm.illinois.edu/", + slug: "WlQDmu", + }; + + const response = await supertest(app.server) + .post("/api/v1/linkry/redir") + .set("Authorization", `Bearer ${userJwt}`) + .send(payload); + + expect(response.statusCode).toBe(400); + expect(response.body.name).toEqual("ValidationError"); +}); + +vi.spyOn(app, "hasRoute").mockImplementation(({ url, method }) => { + return url === "/reserved" && method === "GET"; +}); + +test("Unhappy path: Linkry slug is reserved", async () => { + ddbMock + .on(QueryCommand, { TableName: genericConfig.LinkryDynamoTableName }) + .resolves({ + Items: [], + }); + + ddbMock.on(TransactWriteItemsCommand).resolves({}); + + const payload = { + access: [], + redirect: "https://www.acm.illinois.edu/", + slug: "reserved", + }; + + const response = await supertest(app.server) + .post("/api/v1/linkry/redir") + .set("Authorization", `Bearer ${adminJwt}`) + .send(payload); + + expect(response.statusCode).toBe(400); + expect(response.body.name).toEqual("ValidationError"); + expect(response.body.message).toEqual( + `Slug reserved is reserved by the system.`, + ); +}); + +test("Unhappy path: TransactionCancelled error", async () => { + ddbMock + .on(QueryCommand, { TableName: genericConfig.LinkryDynamoTableName }) + .resolves({ + Items: [], + }); + + ddbMock.on(TransactWriteItemsCommand).rejects( + new TransactionCanceledException({ + $metadata: {}, + message: "Transaction intentionally cancelled", + CancellationReasons: [ + { + Code: "ConditionalCheckFailed", + Message: "Transaction intentionally cancelled", + }, + ], + }), + ); + + const payload = { + access: [], + redirect: "https://www.acm.illinois.edu/", + slug: "ABCDEFG", + }; + + const response = await supertest(app.server) + .post("/api/v1/linkry/redir") + .set("Authorization", `Bearer ${adminJwt}`) + .send(payload); + + expect(response.statusCode).toBe(400); + expect(response.body.name).toEqual("ValidationError"); + expect(response.body.message).toEqual( + "The record was modified by another process. Please try again.", + ); +}); + +test("Unhappy path: Database error", async () => { + ddbMock + .on(QueryCommand, { TableName: genericConfig.LinkryDynamoTableName }) + .resolves({ + Items: [], + }); + + ddbMock.on(TransactWriteItemsCommand).rejects(); + + const payload = { + access: [], + redirect: "https://www.acm.illinois.edu/", + slug: "ABCDEFG", + }; + + const response = await supertest(app.server) + .post("/api/v1/linkry/redir") + .set("Authorization", `Bearer ${adminJwt}`) + .send(payload); + + expect(response.statusCode).toBe(500); + expect(response.body.name).toEqual("DatabaseInsertError"); + expect(response.body.message).toEqual(`Failed to save data to DynamoDB.`); +}); + +//Delete Link +test("Happy path: Delete linkry success", async () => { + const userJwt = createJwt( + undefined, + ["LINKS_MANAGER", "940e4f9e-6891-4e28-9e29-148798495cdb"], + "alice@illinois.edu", + ); + ddbMock + .on(QueryCommand, { TableName: genericConfig.LinkryDynamoTableName }) + .resolves({ + Items: dynamoTableData, + }); + + ddbMock.on(TransactWriteItemsCommand).resolves({}); + + const response = await supertest(app.server) + .delete("/api/v1/linkry/redir/WLQDmu") + .set("Authorization", `Bearer ${userJwt}`); + + expect(response.statusCode).toBe(204); +}); + +test("Unhappy path: Delete linkry slug not found/invalid", async () => { + const userJwt = createJwt(undefined, ["LINKS_MANAGER"], "alice@illinois.edu"); + ddbMock + .on(QueryCommand, { TableName: genericConfig.LinkryDynamoTableName }) + .resolves({ + Items: [], + }); + + ddbMock.on(TransactWriteItemsCommand).resolves({}); + + const response = await supertest(app.server) + .delete("/api/v1/linkry/redir/invalid") + .set("Authorization", `Bearer ${userJwt}`); + + expect(response.statusCode).toBe(404); + expect(response.body.name).toEqual("NotFoundError"); +}); + +test("Unhappy path: Delete linkry Invalid Access", async () => { + const userJwt = createJwt( + undefined, + ["LINKS_MANAGER", "InvalidGroupId22"], + "alice@illinois.edu", + ); + ddbMock + .on(QueryCommand, { TableName: genericConfig.LinkryDynamoTableName }) + .resolves({ + Items: dynamoTableData, + }); + + ddbMock.on(TransactWriteItemsCommand).resolves({}); + + const response = await supertest(app.server) + .delete("/api/v1/linkry/redir/WLQDmu") + .set("Authorization", `Bearer ${userJwt}`); + + expect(response.statusCode).toBe(401); + expect(response.body.name).toEqual("UnauthorizedError"); +}); + +test("Unhappy path: TransactionCancelled error", async () => { + ddbMock + .on(QueryCommand, { TableName: genericConfig.LinkryDynamoTableName }) + .resolves({ + Items: dynamoTableData, + }); + + ddbMock.on(TransactWriteItemsCommand).rejects( + new TransactionCanceledException({ + $metadata: {}, + message: "Transaction intentionally cancelled", + CancellationReasons: [ + { + Code: "ConditionalCheckFailed", + Message: "Transaction intentionally cancelled", + }, + ], + }), + ); + + const response = await supertest(app.server) + .delete("/api/v1/linkry/redir/WLQDmu") + .set("Authorization", `Bearer ${adminJwt}`); + + expect(response.statusCode).toBe(400); + expect(response.body.name).toEqual("ValidationError"); + expect(response.body.message).toEqual( + "The record was modified by another process. Please try again.", + ); +}); + +test("Unhappy path: Delete linkry Database Error", async () => { + ddbMock + .on(QueryCommand, { TableName: genericConfig.LinkryDynamoTableName }) + .resolves({ + Items: dynamoTableData, + }); + + ddbMock.on(TransactWriteItemsCommand).rejects(); + + const response = await supertest(app.server) + .delete("/api/v1/linkry/redir/WLQDmu") + .set("Authorization", `Bearer ${adminJwt}`); + + expect(response.statusCode).toBe(500); + expect(response.body.name).toEqual("DatabaseDeleteError"); +}); + +//Get Link by Slug + +test("Happy path: Get Delegated Link by Slug Correct Access", async () => { + const userJwt = createJwt( + undefined, + ["LINKS_MANAGER", "940e4f9e-6891-4e28-9e29-148798495cdb"], + "cloud@illinois.edu", + ); + ddbMock + .on(QueryCommand, { TableName: genericConfig.LinkryDynamoTableName }) + .resolves({ + Items: dynamoTableData, + }); + + const response = await app.inject({ + method: "GET", + url: "/api/v1/linkry/redir/WlQDmu", + headers: { + Authorization: `Bearer ${userJwt}`, + }, + }); + expect(response.statusCode).toBe(200); + let body = JSON.parse(response.body); + expect(body).toEqual({ + slug: "WlQDmu", + access: [ + "f8dfc4cf-456b-4da3-9053-f7fdeda5d5d6", + "940e4f9e-6891-4e28-9e29-148798495cdb", + ], + createdAt: "2025-04-18T18:36:50.706Z", + redirect: "https://www.gmaill.com/", + updatedAt: "2025-04-18T18:37:40.681Z", + owner: "bob@illinois.edu", + }); +}); + +test("Happy path: Get Delegated Link by Slug Admin Access", async () => { + const userJwt = createJwt(undefined, ["LINKS_ADMIN"], "test@illinois.edu"); + ddbMock + .on(QueryCommand, { TableName: genericConfig.LinkryDynamoTableName }) + .resolves({ + Items: dynamoTableData, + }); + + const response = await app.inject({ + method: "GET", + url: "/api/v1/linkry/redir/WlQDmu", + headers: { + Authorization: `Bearer ${userJwt}`, + }, + }); + expect(response.statusCode).toBe(200); + let body = JSON.parse(response.body); + expect(body).toEqual({ + slug: "WlQDmu", + access: [ + "f8dfc4cf-456b-4da3-9053-f7fdeda5d5d6", + "940e4f9e-6891-4e28-9e29-148798495cdb", + ], + createdAt: "2025-04-18T18:36:50.706Z", + redirect: "https://www.gmaill.com/", + updatedAt: "2025-04-18T18:37:40.681Z", + owner: "bob@illinois.edu", + }); +}); + +test("Unhappy path: Get Delegated Link by Slug Incorrect Access", async () => { + const userJwt = createJwt( + undefined, + ["LINKS_MANAGER", "NotValidGroupId222"], + "cloud@illinois.edu", + ); + ddbMock + .on(QueryCommand, { TableName: genericConfig.LinkryDynamoTableName }) + .resolves({ + Items: dynamoTableData, + }); + + const response = await app.inject({ + method: "GET", + url: "/api/v1/linkry/redir/WlQDmu", + headers: { + Authorization: `Bearer ${userJwt}`, + }, + }); + expect(response.statusCode).toBe(401); + let body = JSON.parse(response.body); + expect(body.name).toEqual("UnauthorizedError"); +}); + +test("Unhappy path: Get Delegated Link by Slug Not Found", async () => { + const userJwt = createJwt(undefined, ["LINKS_ADMIN"], "cloud@illinois.edu"); + + ddbMock + .on(QueryCommand, { TableName: genericConfig.LinkryDynamoTableName }) + .resolves({ + Items: [], + }); + + const response = await app.inject({ + method: "GET", + url: "/api/v1/linkry/redir/WlQDmu", + headers: { + Authorization: `Bearer ${userJwt}`, + }, + }); + expect(response.statusCode).toBe(404); + let body = JSON.parse(response.body); + expect(body.name).toEqual("NotFoundError"); +}); + +test("Unhappy path: Get Delegated Link by Slug Database Error", async () => { + ddbMock + .on(QueryCommand, { TableName: genericConfig.LinkryDynamoTableName }) + .rejects(); + + const response = await app.inject({ + method: "GET", + url: "/api/v1/linkry/redir/WlQDmu", + headers: { + Authorization: `Bearer ${adminJwt}`, + }, + }); + + expect(response.statusCode).toBe(500); + let body = JSON.parse(response.body); + expect(body.name).toEqual("DatabaseFetchError"); +}); diff --git a/tests/unit/mockLinkryData.testdata.ts b/tests/unit/mockLinkryData.testdata.ts new file mode 100644 index 00000000..049779e4 --- /dev/null +++ b/tests/unit/mockLinkryData.testdata.ts @@ -0,0 +1,49 @@ +const linkry1 = { + slug: { + S: "WlQDmu", + }, + access: { + S: "OWNER#bob@illinois.edu", + }, + createdAt: { + S: "2025-04-18T18:36:50.706Z", + }, + redirect: { + S: "https://www.gmaill.com/", + }, + updatedAt: { + S: "2025-04-18T18:37:40.681Z", + }, +}; +const linkry2 = { + slug: { + S: "WlQDmu", + }, + access: { + S: "GROUP#f8dfc4cf-456b-4da3-9053-f7fdeda5d5d6", + }, + createdAt: { + S: "2025-04-18T18:36:50.706Z", + }, + updatedAt: { + S: "2025-04-18T18:37:40.681Z", + }, +}; +const linkry3 = { + slug: { + S: "WlQDmu", + }, + access: { + S: "GROUP#940e4f9e-6891-4e28-9e29-148798495cdb", + }, + createdAt: { + S: "2025-04-18T18:36:50.706Z", + }, + updatedAt: { + S: "2025-04-18T18:37:40.681Z", + }, +}; + +const dynamoTableData = [linkry1, linkry2, linkry3]; + +export { dynamoTableData }; diff --git a/tests/unit/roomRequests.test.ts b/tests/unit/roomRequests.test.ts new file mode 100644 index 00000000..5183aceb --- /dev/null +++ b/tests/unit/roomRequests.test.ts @@ -0,0 +1,523 @@ +import { afterAll, expect, test, beforeEach, vi, describe } from "vitest"; +import init from "../../src/api/index.js"; +import { + GetSecretValueCommand, + SecretsManagerClient, +} from "@aws-sdk/client-secrets-manager"; +import { mockClient } from "aws-sdk-client-mock"; +import { secretJson } from "./secret.testdata.js"; +import { + DynamoDBClient, + PutItemCommand, + QueryCommand, + ScanCommand, + TransactWriteItemsCommand, +} from "@aws-sdk/client-dynamodb"; +import supertest from "supertest"; +import { createJwt } from "./auth.test.js"; +import { v4 as uuidv4 } from "uuid"; +import { marshall } from "@aws-sdk/util-dynamodb"; +import { environmentConfig, genericConfig } from "../../src/common/config.js"; +import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs"; +import { AvailableSQSFunctions } from "../../src/common/types/sqsMessage.js"; +import { RoomRequestStatus } from "../../src/common/types/roomRequest.js"; + +const smMock = mockClient(SecretsManagerClient); +const ddbMock = mockClient(DynamoDBClient); +const sqsMock = mockClient(SQSClient); + +const app = await init(); +describe("Test Room Request Creation", async () => { + const testRequestId = "test-request-id"; + const testSemesterId = "sp25"; + const statusBody = { + status: RoomRequestStatus.APPROVED, + notes: "Request approved by committee.", + }; + const makeUrl = () => + `/api/v1/roomRequests/${testSemesterId}/${testRequestId}/status`; + test("Unauthenticated access (missing token)", async () => { + await app.ready(); + const response = await supertest(app.server) + .post("/api/v1/roomRequests") + .send({ + invoiceId: "ACM102", + invoiceAmountUsd: 100, + contactName: "John Doe", + contactEmail: "john@example.com", + }); + expect(response.statusCode).toBe(403); + }); + test("Validation failure: Missing required fields", async () => { + await app.ready(); + const testJwt = createJwt(); + const response = await supertest(app.server) + .post("/api/v1/roomRequests") + .set("authorization", `Bearer ${testJwt}`) + .send({}); + expect(response.statusCode).toBe(400); + }); + test("Virtual reservation request is accepted", async () => { + await app.ready(); + const testJwt = createJwt(); + ddbMock.on(TransactWriteItemsCommand).resolvesOnce({}).rejects(); + const roomRequest = { + host: "Infrastructure Committee", + title: "Testing", + theme: "Athletics", + semester: "sp25", + description: " f f f f f f f f f f f f f f ffffff", + eventStart: "2025-04-24T18:00:30.679Z", + eventEnd: "2025-04-24T19:00:30.679Z", + isRecurring: false, + setupNeeded: false, + hostingMinors: false, + locationType: "virtual", + onCampusPartners: null, + offCampusPartners: null, + nonIllinoisSpeaker: null, + nonIllinoisAttendees: null, + foodOrDrink: false, + crafting: false, + comments: "", + }; + const response = await supertest(app.server) + .post("/api/v1/roomRequests") + .set("authorization", `Bearer ${testJwt}`) + .send(roomRequest); + expect(response.statusCode).toBe(201); + expect(ddbMock.calls().length).toEqual(1); + }); + test("Hybrid reservation request is accepted", async () => { + await app.ready(); + const testJwt = createJwt(); + ddbMock.on(TransactWriteItemsCommand).resolvesOnce({}).rejects(); + const roomRequest = { + host: "Infrastructure Committee", + title: "Testing", + theme: "Athletics", + semester: "sp25", + description: " f f f f f f f f f f f f f f ffffff", + eventStart: "2025-04-24T18:00:30.679Z", + eventEnd: "2025-04-24T19:00:30.679Z", + isRecurring: false, + setupNeeded: false, + hostingMinors: false, + locationType: "both", + spaceType: "campus_classroom", + specificRoom: "None", + estimatedAttendees: 10, + seatsNeeded: 20, + onCampusPartners: null, + offCampusPartners: null, + nonIllinoisSpeaker: null, + nonIllinoisAttendees: null, + foodOrDrink: false, + crafting: false, + comments: "", + }; + const response = await supertest(app.server) + .post("/api/v1/roomRequests") + .set("authorization", `Bearer ${testJwt}`) + .send(roomRequest); + expect(response.statusCode).toBe(201); + expect(ddbMock.calls().length).toEqual(1); + }); + test("Validation failure: eventEnd before eventStart", async () => { + const testJwt = createJwt(); + ddbMock.rejects(); + const response = await supertest(app.server) + .post("/api/v1/roomRequests") + .set("authorization", `Bearer ${testJwt}`) + .send({ + host: "Infrastructure Committee", + title: "Valid Title", + semester: "sp25", + theme: "Athletics", + description: "This is a valid description with at least ten words.", + eventStart: "2025-04-25T12:00:00Z", + eventEnd: "2025-04-25T10:00:00Z", + isRecurring: false, + setupNeeded: false, + hostingMinors: false, + locationType: "virtual", + foodOrDrink: false, + crafting: false, + onCampusPartners: null, + offCampusPartners: null, + nonIllinoisSpeaker: null, + nonIllinoisAttendees: null, + }); + expect(response.statusCode).toBe(400); + expect(response.body.message).toContain( + "End date/time must be after start date/time", + ); + expect(ddbMock.calls.length).toEqual(0); + }); + test("Validation failure: isRecurring without recurrencePattern and endDate", async () => { + const testJwt = createJwt(); + ddbMock.rejects(); + const response = await supertest(app.server) + .post("/api/v1/roomRequests") + .set("authorization", `Bearer ${testJwt}`) + .send({ + host: "Infrastructure Committee", + title: "Recurring Event", + semester: "sp25", + theme: "Athletics", + description: + "This description includes enough words to pass the test easily.", + eventStart: "2025-04-25T12:00:00Z", + eventEnd: "2025-04-25T13:00:00Z", + isRecurring: true, + setupNeeded: false, + hostingMinors: false, + locationType: "virtual", + foodOrDrink: false, + crafting: false, + onCampusPartners: null, + offCampusPartners: null, + nonIllinoisSpeaker: null, + nonIllinoisAttendees: null, + }); + expect(response.statusCode).toBe(400); + expect(response.body.message).toContain( + "Please select a recurrence pattern", + ); + expect(response.body.message).toContain( + "Please select an end date for the recurring event", + ); + expect(ddbMock.calls.length).toEqual(0); + }); + test("Validation failure: setupNeeded is true without setupMinutesBefore", async () => { + const testJwt = createJwt(); + ddbMock.rejects(); + + const response = await supertest(app.server) + .post("/api/v1/roomRequests") + .set("authorization", `Bearer ${testJwt}`) + .send({ + host: "Infrastructure Committee", + title: "Setup Event", + semester: "sp25", + theme: "Athletics", + description: + "Wordy description that definitely contains more than ten words easily.", + eventStart: "2025-04-25T12:00:00Z", + eventEnd: "2025-04-25T13:00:00Z", + isRecurring: false, + setupNeeded: true, + hostingMinors: false, + locationType: "virtual", + foodOrDrink: false, + crafting: false, + onCampusPartners: null, + offCampusPartners: null, + nonIllinoisSpeaker: null, + nonIllinoisAttendees: null, + }); + + expect(response.statusCode).toBe(400); + expect(response.body.message).toContain( + "how many minutes before the event", + ); + expect(ddbMock.calls()).toHaveLength(0); + }); + test("Validation failure: in-person event missing spaceType, room, seats", async () => { + const testJwt = createJwt(); + ddbMock.rejects(); + + const response = await supertest(app.server) + .post("/api/v1/roomRequests") + .set("authorization", `Bearer ${testJwt}`) + .send({ + host: "Infrastructure Committee", + title: "Physical Event", + semester: "sp25", + theme: "Athletics", + description: + "This description has more than enough words to satisfy the validator.", + eventStart: "2025-04-25T12:00:00Z", + eventEnd: "2025-04-25T13:00:00Z", + isRecurring: false, + setupNeeded: false, + hostingMinors: false, + locationType: "in-person", + foodOrDrink: false, + crafting: false, + onCampusPartners: null, + offCampusPartners: null, + nonIllinoisSpeaker: null, + nonIllinoisAttendees: null, + }); + + expect(response.statusCode).toBe(400); + expect(response.body.message).toContain("Please select a space type"); + expect(response.body.message).toContain( + "Please provide details about the room location", + ); + expect(response.body.message).toContain( + "Please provide an estimated number of attendees", + ); + expect(response.body.message).toContain( + "Please specify how many seats you need", + ); + expect(ddbMock.calls()).toHaveLength(0); + }); + test("Validation failure: seatsNeeded < estimatedAttendees", async () => { + const testJwt = createJwt(); + ddbMock.rejects(); + + const response = await supertest(app.server) + .post("/api/v1/roomRequests") + .set("authorization", `Bearer ${testJwt}`) + .send({ + host: "Infrastructure Committee", + title: "Seats Mismatch", + semester: "sp25", + theme: "Athletics", + description: + "Description with lots of words to ensure it is long enough to pass validation.", + eventStart: "2025-04-25T12:00:00Z", + eventEnd: "2025-04-25T13:00:00Z", + isRecurring: false, + setupNeeded: false, + hostingMinors: false, + locationType: "in-person", + spaceType: "campus_classroom", + specificRoom: "Room 101", + estimatedAttendees: 20, + seatsNeeded: 10, + foodOrDrink: false, + crafting: false, + onCampusPartners: null, + offCampusPartners: null, + nonIllinoisSpeaker: null, + nonIllinoisAttendees: null, + }); + + expect(response.statusCode).toBe(400); + expect(response.body.message).toContain( + "Number of seats must be greater than or equal to number of attendees", + ); + expect(ddbMock.calls()).toHaveLength(0); + }); + test("Successful request writes 3 items to DynamoDB transaction", async () => { + const testJwt = createJwt(); + + ddbMock.on(TransactWriteItemsCommand).callsFake((input) => { + expect(input.TransactItems).toHaveLength(3); + + const tableNames = input.TransactItems.map( + (item: Record) => Object.values(item)[0].TableName, + ); + + expect(tableNames).toEqual( + expect.arrayContaining([ + genericConfig.RoomRequestsTableName, + genericConfig.RoomRequestsStatusTableName, + genericConfig.AuditLogTable, + ]), + ); + + return { $metadata: { httpStatusCode: 200 } }; + }); + + const roomRequest = { + host: "Infrastructure Committee", + title: "Valid Request", + semester: "sp25", + theme: "Athletics", + description: + "This is a valid request with enough words in the description field.", + eventStart: new Date("2025-04-24T12:00:00Z"), + eventEnd: new Date("2025-04-24T13:00:00Z"), + isRecurring: false, + setupNeeded: false, + hostingMinors: false, + locationType: "virtual", + foodOrDrink: false, + crafting: false, + onCampusPartners: null, + offCampusPartners: null, + nonIllinoisSpeaker: null, + nonIllinoisAttendees: null, + }; + + const response = await supertest(app.server) + .post("/api/v1/roomRequests") + .set("authorization", `Bearer ${testJwt}`) + .send(roomRequest); + + expect(response.statusCode).toBe(201); + expect(ddbMock.commandCalls(TransactWriteItemsCommand).length).toBe(1); + }); + test("Successful request queues a message to SQS", async () => { + const testJwt = createJwt(); + + // Mock DynamoDB transaction success + ddbMock.on(TransactWriteItemsCommand).resolves({}); + + // Mock SQS response + sqsMock.on(SendMessageCommand).resolves({ MessageId: "mocked-message-id" }); + + const roomRequest = { + host: "Infrastructure Committee", + title: "Valid SQS Request", + semester: "sp25", + theme: "Athletics", + description: + "A well-formed description that has at least ten total words.", + eventStart: new Date("2025-04-24T12:00:00Z"), + eventEnd: new Date("2025-04-24T13:00:00Z"), + isRecurring: false, + setupNeeded: false, + hostingMinors: false, + locationType: "virtual", + foodOrDrink: false, + crafting: false, + onCampusPartners: null, + offCampusPartners: null, + nonIllinoisSpeaker: null, + nonIllinoisAttendees: null, + }; + + const response = await supertest(app.server) + .post("/api/v1/roomRequests") + .set("authorization", `Bearer ${testJwt}`) + .send(roomRequest); + + expect(response.statusCode).toBe(201); + expect(sqsMock.commandCalls(SendMessageCommand).length).toBe(1); + + const sent = sqsMock.commandCalls(SendMessageCommand)[0].args[0] + .input as SendMessageCommand["input"]; + + expect(sent.QueueUrl).toBe(environmentConfig["dev"].SqsQueueUrl); + expect(JSON.parse(sent.MessageBody as string)).toMatchObject({ + function: AvailableSQSFunctions.EmailNotifications, + payload: { + subject: expect.stringContaining("New Room Reservation Request"), + }, + }); + }); + afterAll(async () => { + await app.close(); + }); + beforeEach(() => { + (app as any).nodeCache.flushAll(); + ddbMock.reset(); + sqsMock.reset(); + vi.clearAllMocks(); + smMock.on(GetSecretValueCommand).resolves({ + SecretString: secretJson, + }); + }); + test("Unauthenticated access is rejected", async () => { + await app.ready(); + const response = await supertest(app.server) + .post(makeUrl()) + .send(statusBody); + + expect(response.statusCode).toBe(403); + }); + + test("Fails if request status with CREATED not found", async () => { + const testJwt = createJwt(); + ddbMock.on(QueryCommand).resolves({ Count: 0, Items: [] }); + ddbMock.rejects(); // ensure no other writes + await app.ready(); + const response = await supertest(app.server) + .post(makeUrl()) + .set("authorization", `Bearer ${testJwt}`) + .send(statusBody); + + expect(response.statusCode).toBe(500); + expect(ddbMock.commandCalls(TransactWriteItemsCommand).length).toBe(0); + }); + + test("Fails if original request found but missing createdBy", async () => { + const testJwt = createJwt(); + ddbMock.on(QueryCommand).resolves({ + Count: 1, + Items: [marshall({})], + }); + await app.ready(); + const response = await supertest(app.server) + .post(makeUrl()) + .set("authorization", `Bearer ${testJwt}`) + .send(statusBody); + + expect(response.statusCode).toBe(500); + expect(response.body.message).toContain( + "Could not find original reservation requestor", + ); + }); + + test("Creates status update with audit log in DynamoDB", async () => { + const testJwt = createJwt(); + + ddbMock.on(QueryCommand).resolves({ + Count: 1, + Items: [marshall({ createdBy: "originalUser" })], + }); + + ddbMock.on(TransactWriteItemsCommand).callsFake((input) => { + expect(input.TransactItems).toHaveLength(2); + + const tableNames = input.TransactItems.map( + (item: Record) => Object.values(item)[0].TableName, + ); + + expect(tableNames).toEqual( + expect.arrayContaining([ + genericConfig.RoomRequestsStatusTableName, + genericConfig.AuditLogTable, + ]), + ); + + return { $metadata: { httpStatusCode: 200 } }; + }); + + sqsMock.on(SendMessageCommand).resolves({ MessageId: "sqs-message-id" }); + await app.ready(); + const response = await supertest(app.server) + .post(makeUrl()) + .set("authorization", `Bearer ${testJwt}`) + .send(statusBody); + + expect(response.statusCode).toBe(201); + expect(ddbMock.commandCalls(TransactWriteItemsCommand).length).toBe(1); + }); + + test("Queues SQS notification after status update", async () => { + const testJwt = createJwt(); + + ddbMock.on(QueryCommand).resolves({ + Count: 1, + Items: [marshall({ createdBy: "originalUser" })], + }); + + ddbMock.on(TransactWriteItemsCommand).resolves({}); + + sqsMock.on(SendMessageCommand).resolves({ MessageId: "mock-sqs-id" }); + await app.ready(); + const response = await supertest(app.server) + .post(makeUrl()) + .set("authorization", `Bearer ${testJwt}`) + .send(statusBody); + + expect(response.statusCode).toBe(201); + expect(sqsMock.commandCalls(SendMessageCommand).length).toBe(1); + + const sent = sqsMock.commandCalls(SendMessageCommand)[0].args[0] + .input as SendMessageCommand["input"]; + + const body = JSON.parse(sent.MessageBody as string); + expect(body.function).toBe(AvailableSQSFunctions.EmailNotifications); + expect(body.payload.subject).toContain( + "Room Reservation Request Status Change", + ); + expect(body.payload.to).toEqual(["originalUser"]); + }); +}); diff --git a/tests/unit/stripe.test.ts b/tests/unit/stripe.test.ts index de4563a8..3a64e87e 100644 --- a/tests/unit/stripe.test.ts +++ b/tests/unit/stripe.test.ts @@ -11,11 +11,13 @@ import { PutItemCommand, QueryCommand, ScanCommand, + TransactWriteItemsCommand, } from "@aws-sdk/client-dynamodb"; import supertest from "supertest"; import { createJwt } from "./auth.test.js"; import { v4 as uuidv4 } from "uuid"; import { marshall } from "@aws-sdk/util-dynamodb"; +import { genericConfig } from "../../src/common/config.js"; const smMock = mockClient(SecretsManagerClient); const ddbMock = mockClient(DynamoDBClient); @@ -103,7 +105,7 @@ describe("Test Stripe link creation", async () => { name: "ValidationError", id: 104, message: - 'String must contain at least 1 character(s) at "invoiceId"; Number must be greater than or equal to 50 at "invoiceAmountUsd"; String must contain at least 1 character(s) at "contactName"; Required at "contactEmail"', + "body/invoiceId String must contain at least 1 character(s), body/invoiceAmountUsd Number must be greater than or equal to 50, body/contactName String must contain at least 1 character(s), body/contactEmail Required", }); expect(ddbMock.calls().length).toEqual(0); expect(smMock.calls().length).toEqual(0); @@ -130,25 +132,26 @@ describe("Test Stripe link creation", async () => { error: true, name: "ValidationError", id: 104, - message: 'Invalid email at "contactEmail"', + message: "body/contactEmail Invalid email", }); expect(ddbMock.calls().length).toEqual(0); expect(smMock.calls().length).toEqual(0); }); test("POST happy path", async () => { - ddbMock.on(PutItemCommand).resolves({}); + const invoicePayload = { + invoiceId: "ACM102", + invoiceAmountUsd: 51, + contactName: "Infra User", + contactEmail: "testing@acm.illinois.edu", + }; + ddbMock.on(TransactWriteItemsCommand).resolvesOnce({}).rejects(); const testJwt = createJwt(); await app.ready(); const response = await supertest(app.server) .post("/api/v1/stripe/paymentLinks") .set("authorization", `Bearer ${testJwt}`) - .send({ - invoiceId: "ACM102", - invoiceAmountUsd: 51, - contactName: "Infra User", - contactEmail: "testing@acm.illinois.edu", - }); + .send(invoicePayload); expect(response.statusCode).toBe(201); expect(response.body).toStrictEqual({ id: linkId, @@ -234,7 +237,7 @@ describe("Test Stripe link creation", async () => { }); const testJwt = createJwt( undefined, - "1", + ["1"], "infra-unit-test-stripeonly@acm.illinois.edu", ); const response = await supertest(app.server) diff --git a/tests/unit/tickets.test.ts b/tests/unit/tickets.test.ts index cff62706..c3e24f80 100644 --- a/tests/unit/tickets.test.ts +++ b/tests/unit/tickets.test.ts @@ -96,7 +96,7 @@ describe("Test getting ticketing + merch metadata", async () => { Items: ticketsMetadata as Record[], }) .rejects(); - const testJwt = createJwt(undefined, "scanner-only"); + const testJwt = createJwt(undefined, ["scanner-only"]); await app.ready(); const response = await supertest(app.server) .get("/api/v1/tickets") @@ -199,8 +199,7 @@ describe("Test ticket purchase verification", async () => { expect(responseDataJson).toEqual({ error: true, id: 104, - message: - 'Invalid literal value, expected "merch" at "type", or Required at "ticketId"', + message: "body/ Invalid input", name: "ValidationError", }); }); @@ -354,7 +353,7 @@ describe("Test merch purchase verification", async () => { expect(responseDataJson).toEqual({ error: true, id: 104, - message: `Required at "email"; Required at "stripePi", or Invalid literal value, expected "ticket" at "type"`, + message: `body/ Invalid input`, name: "ValidationError", }); }); diff --git a/tests/unit/vitest.setup.ts b/tests/unit/vitest.setup.ts index dab8afdf..241eec49 100644 --- a/tests/unit/vitest.setup.ts +++ b/tests/unit/vitest.setup.ts @@ -1,3 +1,4 @@ +import "zod-openapi/extend"; import { vi, afterEach } from "vitest"; import { allAppRoles, AppRoles } from "../../src/common/roles.js"; import { DynamoDBClient, QueryCommand } from "@aws-sdk/client-dynamodb"; @@ -43,6 +44,8 @@ vi.mock( "0": allAppRoles, "1": [], "scanner-only": [AppRoles.TICKETS_SCANNER], + LINKS_ADMIN: [AppRoles.LINKS_ADMIN], + LINKS_MANAGER: [AppRoles.LINKS_MANAGER], }; return mockGroupRoles[groupId] || []; diff --git a/yarn.lock b/yarn.lock index 61f617c6..6e1f3ed0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28,7 +28,7 @@ "@aws-crypto/sha256-browser@5.2.0": version "5.2.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz#153895ef1dba6f9fce38af550e0ef58988eb649e" + resolved "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz" integrity sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw== dependencies: "@aws-crypto/sha256-js" "^5.2.0" @@ -41,7 +41,7 @@ "@aws-crypto/sha256-js@5.2.0", "@aws-crypto/sha256-js@^5.2.0": version "5.2.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz#c4fdb773fdbed9a664fc1a95724e206cf3860042" + resolved "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz" integrity sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA== dependencies: "@aws-crypto/util" "^5.2.0" @@ -50,20 +50,66 @@ "@aws-crypto/supports-web-crypto@^5.2.0": version "5.2.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz#a1e399af29269be08e695109aa15da0a07b5b5fb" + resolved "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz" integrity sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg== dependencies: tslib "^2.6.2" "@aws-crypto/util@^5.2.0": version "5.2.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-5.2.0.tgz#71284c9cffe7927ddadac793c14f14886d3876da" + resolved "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz" integrity sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ== dependencies: "@aws-sdk/types" "^3.222.0" "@smithy/util-utf8" "^2.0.0" tslib "^2.6.2" +"@aws-sdk/client-cloudfront-keyvaluestore@^3.787.0": + version "3.787.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-cloudfront-keyvaluestore/-/client-cloudfront-keyvaluestore-3.787.0.tgz#88f6c97842c6f4ed0cb4a934b0305fe5199b56aa" + integrity sha512-PIzXmcsboPcluw+N0WTCVzpaybJbwLbW0EPulalEZK9a4F6VzWdFdfrdAKNHM65JXfzlXKrerdxnW6UPcsO3Kg== + dependencies: + "@aws-crypto/sha256-browser" "5.2.0" + "@aws-crypto/sha256-js" "5.2.0" + "@aws-sdk/core" "3.775.0" + "@aws-sdk/credential-provider-node" "3.787.0" + "@aws-sdk/middleware-host-header" "3.775.0" + "@aws-sdk/middleware-logger" "3.775.0" + "@aws-sdk/middleware-recursion-detection" "3.775.0" + "@aws-sdk/middleware-user-agent" "3.787.0" + "@aws-sdk/region-config-resolver" "3.775.0" + "@aws-sdk/signature-v4-multi-region" "3.775.0" + "@aws-sdk/types" "3.775.0" + "@aws-sdk/util-endpoints" "3.787.0" + "@aws-sdk/util-user-agent-browser" "3.775.0" + "@aws-sdk/util-user-agent-node" "3.787.0" + "@smithy/config-resolver" "^4.1.0" + "@smithy/core" "^3.2.0" + "@smithy/fetch-http-handler" "^5.0.2" + "@smithy/hash-node" "^4.0.2" + "@smithy/invalid-dependency" "^4.0.2" + "@smithy/middleware-content-length" "^4.0.2" + "@smithy/middleware-endpoint" "^4.1.0" + "@smithy/middleware-retry" "^4.1.0" + "@smithy/middleware-serde" "^4.0.3" + "@smithy/middleware-stack" "^4.0.2" + "@smithy/node-config-provider" "^4.0.2" + "@smithy/node-http-handler" "^4.0.4" + "@smithy/protocol-http" "^5.1.0" + "@smithy/smithy-client" "^4.2.0" + "@smithy/types" "^4.2.0" + "@smithy/url-parser" "^4.0.2" + "@smithy/util-base64" "^4.0.0" + "@smithy/util-body-length-browser" "^4.0.0" + "@smithy/util-body-length-node" "^4.0.0" + "@smithy/util-defaults-mode-browser" "^4.0.8" + "@smithy/util-defaults-mode-node" "^4.0.8" + "@smithy/util-endpoints" "^3.0.2" + "@smithy/util-middleware" "^4.0.2" + "@smithy/util-retry" "^4.0.2" + "@smithy/util-utf8" "^4.0.0" + tslib "^2.6.2" + "@aws-sdk/client-dynamodb@^3.624.0": version "3.741.0" resolved "https://registry.yarnpkg.com/@aws-sdk/client-dynamodb/-/client-dynamodb-3.741.0.tgz#ed45e1f77ed7bbaf1dcb70b2cae2d20efe609162" @@ -341,6 +387,50 @@ "@smithy/util-utf8" "^4.0.0" tslib "^2.6.2" +"@aws-sdk/client-sso@3.787.0": + version "3.787.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.787.0.tgz#39f1182296b586cb957b449b5f0dabd8f378cf1a" + integrity sha512-L8R+Mh258G0DC73ktpSVrG4TT9i2vmDLecARTDR/4q5sRivdDQSL5bUp3LKcK80Bx+FRw3UETIlX6mYMLL9PJQ== + dependencies: + "@aws-crypto/sha256-browser" "5.2.0" + "@aws-crypto/sha256-js" "5.2.0" + "@aws-sdk/core" "3.775.0" + "@aws-sdk/middleware-host-header" "3.775.0" + "@aws-sdk/middleware-logger" "3.775.0" + "@aws-sdk/middleware-recursion-detection" "3.775.0" + "@aws-sdk/middleware-user-agent" "3.787.0" + "@aws-sdk/region-config-resolver" "3.775.0" + "@aws-sdk/types" "3.775.0" + "@aws-sdk/util-endpoints" "3.787.0" + "@aws-sdk/util-user-agent-browser" "3.775.0" + "@aws-sdk/util-user-agent-node" "3.787.0" + "@smithy/config-resolver" "^4.1.0" + "@smithy/core" "^3.2.0" + "@smithy/fetch-http-handler" "^5.0.2" + "@smithy/hash-node" "^4.0.2" + "@smithy/invalid-dependency" "^4.0.2" + "@smithy/middleware-content-length" "^4.0.2" + "@smithy/middleware-endpoint" "^4.1.0" + "@smithy/middleware-retry" "^4.1.0" + "@smithy/middleware-serde" "^4.0.3" + "@smithy/middleware-stack" "^4.0.2" + "@smithy/node-config-provider" "^4.0.2" + "@smithy/node-http-handler" "^4.0.4" + "@smithy/protocol-http" "^5.1.0" + "@smithy/smithy-client" "^4.2.0" + "@smithy/types" "^4.2.0" + "@smithy/url-parser" "^4.0.2" + "@smithy/util-base64" "^4.0.0" + "@smithy/util-body-length-browser" "^4.0.0" + "@smithy/util-body-length-node" "^4.0.0" + "@smithy/util-defaults-mode-browser" "^4.0.8" + "@smithy/util-defaults-mode-node" "^4.0.8" + "@smithy/util-endpoints" "^3.0.2" + "@smithy/util-middleware" "^4.0.2" + "@smithy/util-retry" "^4.0.2" + "@smithy/util-utf8" "^4.0.0" + tslib "^2.6.2" + "@aws-sdk/client-sts@^3.758.0": version "3.758.0" resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.758.0.tgz#8f2f15e36dd8e00e520d9ea9fe4814aa513d1dbb" @@ -420,6 +510,23 @@ fast-xml-parser "4.4.1" tslib "^2.6.2" +"@aws-sdk/core@3.775.0": + version "3.775.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.775.0.tgz#5d22ba78f07c07b48fb4d5b18172b9a896c0cbd0" + integrity sha512-8vpW4WihVfz0DX+7WnnLGm3GuQER++b0IwQG35JlQMlgqnc44M//KbJPsIHA0aJUJVwJAEShgfr5dUbY8WUzaA== + dependencies: + "@aws-sdk/types" "3.775.0" + "@smithy/core" "^3.2.0" + "@smithy/node-config-provider" "^4.0.2" + "@smithy/property-provider" "^4.0.2" + "@smithy/protocol-http" "^5.1.0" + "@smithy/signature-v4" "^5.0.2" + "@smithy/smithy-client" "^4.2.0" + "@smithy/types" "^4.2.0" + "@smithy/util-middleware" "^4.0.2" + fast-xml-parser "4.4.1" + tslib "^2.6.2" + "@aws-sdk/credential-provider-env@3.734.0": version "3.734.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.734.0.tgz#6c0b1734764a7fb1616455836b1c3dacd99e50a3" @@ -442,6 +549,17 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@aws-sdk/credential-provider-env@3.775.0": + version "3.775.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.775.0.tgz#b8c81818f4c62d89b5f04dc410ab9b48e954f22c" + integrity sha512-6ESVxwCbGm7WZ17kY1fjmxQud43vzJFoLd4bmlR+idQSWdqlzGDYdcfzpjDKTcivdtNrVYmFvcH1JBUwCRAZhw== + dependencies: + "@aws-sdk/core" "3.775.0" + "@aws-sdk/types" "3.775.0" + "@smithy/property-provider" "^4.0.2" + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@aws-sdk/credential-provider-http@3.734.0": version "3.734.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.734.0.tgz#21c5fbb380d1dd503491897b346e1e0b1d06ae41" @@ -474,6 +592,22 @@ "@smithy/util-stream" "^4.1.2" tslib "^2.6.2" +"@aws-sdk/credential-provider-http@3.775.0": + version "3.775.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.775.0.tgz#0fbc7f4e6cada37fc9b647de0d7c12a42a44bcc6" + integrity sha512-PjDQeDH/J1S0yWV32wCj2k5liRo0ssXMseCBEkCsD3SqsU8o5cU82b0hMX4sAib/RkglCSZqGO0xMiN0/7ndww== + dependencies: + "@aws-sdk/core" "3.775.0" + "@aws-sdk/types" "3.775.0" + "@smithy/fetch-http-handler" "^5.0.2" + "@smithy/node-http-handler" "^4.0.4" + "@smithy/property-provider" "^4.0.2" + "@smithy/protocol-http" "^5.1.0" + "@smithy/smithy-client" "^4.2.0" + "@smithy/types" "^4.2.0" + "@smithy/util-stream" "^4.2.0" + tslib "^2.6.2" + "@aws-sdk/credential-provider-ini@3.741.0": version "3.741.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.741.0.tgz#cfe37d5028dc636e49f044f825b05de087f208c4" @@ -512,6 +646,25 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@aws-sdk/credential-provider-ini@3.787.0": + version "3.787.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.787.0.tgz#906ece004141462ae695504b6c07d1200688fd6c" + integrity sha512-hc2taRoDlXn2uuNuHWDJljVWYrp3r9JF1a/8XmOAZhVUNY+ImeeStylHXhXXKEA4JOjW+5PdJj0f1UDkVCHJiQ== + dependencies: + "@aws-sdk/core" "3.775.0" + "@aws-sdk/credential-provider-env" "3.775.0" + "@aws-sdk/credential-provider-http" "3.775.0" + "@aws-sdk/credential-provider-process" "3.775.0" + "@aws-sdk/credential-provider-sso" "3.787.0" + "@aws-sdk/credential-provider-web-identity" "3.787.0" + "@aws-sdk/nested-clients" "3.787.0" + "@aws-sdk/types" "3.775.0" + "@smithy/credential-provider-imds" "^4.0.2" + "@smithy/property-provider" "^4.0.2" + "@smithy/shared-ini-file-loader" "^4.0.2" + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@aws-sdk/credential-provider-node@3.741.0": version "3.741.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.741.0.tgz#29e42e9c4f1be5c3bfa05a10998d6431a432f936" @@ -548,6 +701,24 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@aws-sdk/credential-provider-node@3.787.0": + version "3.787.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.787.0.tgz#3e5cdafb0fecca25b7430f848cbca85000b25c33" + integrity sha512-JioVi44B1vDMaK2CdzqimwvJD3uzvzbQhaEWXsGMBcMcNHajXAXf08EF50JG3ZhLrhhUsT1ObXpbTaPINOhh+g== + dependencies: + "@aws-sdk/credential-provider-env" "3.775.0" + "@aws-sdk/credential-provider-http" "3.775.0" + "@aws-sdk/credential-provider-ini" "3.787.0" + "@aws-sdk/credential-provider-process" "3.775.0" + "@aws-sdk/credential-provider-sso" "3.787.0" + "@aws-sdk/credential-provider-web-identity" "3.787.0" + "@aws-sdk/types" "3.775.0" + "@smithy/credential-provider-imds" "^4.0.2" + "@smithy/property-provider" "^4.0.2" + "@smithy/shared-ini-file-loader" "^4.0.2" + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@aws-sdk/credential-provider-process@3.734.0": version "3.734.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.734.0.tgz#eb1de678a9c3d2d7b382e74a670fa283327f9c45" @@ -572,6 +743,18 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@aws-sdk/credential-provider-process@3.775.0": + version "3.775.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.775.0.tgz#7ab90383f12461c5d20546e933924e654660542b" + integrity sha512-A6k68H9rQp+2+7P7SGO90Csw6nrUEm0Qfjpn9Etc4EboZhhCLs9b66umUsTsSBHus4FDIe5JQxfCUyt1wgNogg== + dependencies: + "@aws-sdk/core" "3.775.0" + "@aws-sdk/types" "3.775.0" + "@smithy/property-provider" "^4.0.2" + "@smithy/shared-ini-file-loader" "^4.0.2" + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@aws-sdk/credential-provider-sso@3.734.0": version "3.734.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.734.0.tgz#68a9d678319e9743d65cf59e2d29c0c440d8975c" @@ -600,6 +783,20 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@aws-sdk/credential-provider-sso@3.787.0": + version "3.787.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.787.0.tgz#77ab6c01e4497d7ff2e6c7f081f3d8695744884b" + integrity sha512-fHc08bsvwm4+dEMEQKnQ7c1irEQmmxbgS+Fq41y09pPvPh31nAhoMcjBSTWAaPHvvsRbTYvmP4Mf12ZGr8/nfg== + dependencies: + "@aws-sdk/client-sso" "3.787.0" + "@aws-sdk/core" "3.775.0" + "@aws-sdk/token-providers" "3.787.0" + "@aws-sdk/types" "3.775.0" + "@smithy/property-provider" "^4.0.2" + "@smithy/shared-ini-file-loader" "^4.0.2" + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@aws-sdk/credential-provider-web-identity@3.734.0": version "3.734.0" resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.734.0.tgz#666b61cc9f498a3aaecd8e38c9ae34aef37e2e64" @@ -624,6 +821,27 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@aws-sdk/credential-provider-web-identity@3.787.0": + version "3.787.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.787.0.tgz#d492d1f4a90b70f3a71a65f11b8d3ef79fb2759e" + integrity sha512-SobmCwNbk6TfEsF283mZPQEI5vV2j6eY5tOCj8Er4Lzraxu9fBPADV+Bib2A8F6jlB1lMPJzOuDCbEasSt/RIw== + dependencies: + "@aws-sdk/core" "3.775.0" + "@aws-sdk/nested-clients" "3.787.0" + "@aws-sdk/types" "3.775.0" + "@smithy/property-provider" "^4.0.2" + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + +"@aws-sdk/crt-loader@3.787.0": + version "3.787.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/crt-loader/-/crt-loader-3.787.0.tgz#570c7e3cc79d20b2391beb1dafe2215343a9ac29" + integrity sha512-NQWFDkYF/lzz2m3icdVr+a0Ua/fN4dij3GPwU+Hr/nzrFR6z7txG3U4m2zkSELJ0PDT4k/1NsgmnQlpyxg0NDg== + dependencies: + "@aws-sdk/util-user-agent-node" "3.787.0" + aws-crt "^1.24.0" + tslib "^2.6.2" + "@aws-sdk/endpoint-cache@3.723.0": version "3.723.0" resolved "https://registry.yarnpkg.com/@aws-sdk/endpoint-cache/-/endpoint-cache-3.723.0.tgz#6c5984698d3cffca4d55f5c1b14350776ee008ac" @@ -654,6 +872,16 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@aws-sdk/middleware-host-header@3.775.0": + version "3.775.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.775.0.tgz#1bf8160b8f4f96ba30c19f9baa030a6c9bd5f94d" + integrity sha512-tkSegM0Z6WMXpLB8oPys/d+umYIocvO298mGvcMCncpRl77L9XkvSLJIFzaHes+o7djAgIduYw8wKIMStFss2w== + dependencies: + "@aws-sdk/types" "3.775.0" + "@smithy/protocol-http" "^5.1.0" + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@aws-sdk/middleware-logger@3.734.0": version "3.734.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.734.0.tgz#d31e141ae7a78667e372953a3b86905bc6124664" @@ -663,6 +891,15 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@aws-sdk/middleware-logger@3.775.0": + version "3.775.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.775.0.tgz#df1909d441cd4bade8d6c7d24c41532808db0e81" + integrity sha512-FaxO1xom4MAoUJsldmR92nT1G6uZxTdNYOFYtdHfd6N2wcNaTuxgjIvqzg5y7QIH9kn58XX/dzf1iTjgqUStZw== + dependencies: + "@aws-sdk/types" "3.775.0" + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@aws-sdk/middleware-recursion-detection@3.734.0": version "3.734.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.734.0.tgz#4fa1deb9887455afbb39130f7d9bc89ccee17168" @@ -673,6 +910,36 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@aws-sdk/middleware-recursion-detection@3.775.0": + version "3.775.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.775.0.tgz#36a40f467754d7c86424d12ef45c05e96ce3475b" + integrity sha512-GLCzC8D0A0YDG5u3F5U03Vb9j5tcOEFhr8oc6PDk0k0vm5VwtZOE6LvK7hcCSoAB4HXyOUM0sQuXrbaAh9OwXA== + dependencies: + "@aws-sdk/types" "3.775.0" + "@smithy/protocol-http" "^5.1.0" + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + +"@aws-sdk/middleware-sdk-s3@3.775.0": + version "3.775.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.775.0.tgz#7b65832ec5a9ccccc8c7337780f722fa59f09d41" + integrity sha512-zsvcu7cWB28JJ60gVvjxPCI7ZU7jWGcpNACPiZGyVtjYXwcxyhXbYEVDSWKsSA6ERpz9XrpLYod8INQWfW3ECg== + dependencies: + "@aws-sdk/core" "3.775.0" + "@aws-sdk/types" "3.775.0" + "@aws-sdk/util-arn-parser" "3.723.0" + "@smithy/core" "^3.2.0" + "@smithy/node-config-provider" "^4.0.2" + "@smithy/protocol-http" "^5.1.0" + "@smithy/signature-v4" "^5.0.2" + "@smithy/smithy-client" "^4.2.0" + "@smithy/types" "^4.2.0" + "@smithy/util-config-provider" "^4.0.0" + "@smithy/util-middleware" "^4.0.2" + "@smithy/util-stream" "^4.2.0" + "@smithy/util-utf8" "^4.0.0" + tslib "^2.6.2" + "@aws-sdk/middleware-sdk-sqs@3.734.0": version "3.734.0" resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.734.0.tgz#65282e8312ae2c6d9c1387533c587c950b71b8af" @@ -711,6 +978,19 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@aws-sdk/middleware-user-agent@3.787.0": + version "3.787.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.787.0.tgz#3d657c0ba1aec72bca079f4691ba20f25569fcfc" + integrity sha512-Lnfj8SmPLYtrDFthNIaNj66zZsBCam+E4XiUDr55DIHTGstH6qZ/q6vg0GfbukxwSmUcGMwSR4Qbn8rb8yd77g== + dependencies: + "@aws-sdk/core" "3.775.0" + "@aws-sdk/types" "3.775.0" + "@aws-sdk/util-endpoints" "3.787.0" + "@smithy/core" "^3.2.0" + "@smithy/protocol-http" "^5.1.0" + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@aws-sdk/nested-clients@3.734.0": version "3.734.0" resolved "https://registry.yarnpkg.com/@aws-sdk/nested-clients/-/nested-clients-3.734.0.tgz#10a116d141522341c446b11783551ef863aabd27" @@ -799,6 +1079,50 @@ "@smithy/util-utf8" "^4.0.0" tslib "^2.6.2" +"@aws-sdk/nested-clients@3.787.0": + version "3.787.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/nested-clients/-/nested-clients-3.787.0.tgz#e8a5a6e7d0b599a7f9f15b900d3223ad080b0a81" + integrity sha512-xk03q1xpKNHgbuo+trEf1dFrI239kuMmjKKsqLEsHlAZbuFq4yRGMlHBrVMnKYOPBhVFDS/VineM991XI52fKg== + dependencies: + "@aws-crypto/sha256-browser" "5.2.0" + "@aws-crypto/sha256-js" "5.2.0" + "@aws-sdk/core" "3.775.0" + "@aws-sdk/middleware-host-header" "3.775.0" + "@aws-sdk/middleware-logger" "3.775.0" + "@aws-sdk/middleware-recursion-detection" "3.775.0" + "@aws-sdk/middleware-user-agent" "3.787.0" + "@aws-sdk/region-config-resolver" "3.775.0" + "@aws-sdk/types" "3.775.0" + "@aws-sdk/util-endpoints" "3.787.0" + "@aws-sdk/util-user-agent-browser" "3.775.0" + "@aws-sdk/util-user-agent-node" "3.787.0" + "@smithy/config-resolver" "^4.1.0" + "@smithy/core" "^3.2.0" + "@smithy/fetch-http-handler" "^5.0.2" + "@smithy/hash-node" "^4.0.2" + "@smithy/invalid-dependency" "^4.0.2" + "@smithy/middleware-content-length" "^4.0.2" + "@smithy/middleware-endpoint" "^4.1.0" + "@smithy/middleware-retry" "^4.1.0" + "@smithy/middleware-serde" "^4.0.3" + "@smithy/middleware-stack" "^4.0.2" + "@smithy/node-config-provider" "^4.0.2" + "@smithy/node-http-handler" "^4.0.4" + "@smithy/protocol-http" "^5.1.0" + "@smithy/smithy-client" "^4.2.0" + "@smithy/types" "^4.2.0" + "@smithy/url-parser" "^4.0.2" + "@smithy/util-base64" "^4.0.0" + "@smithy/util-body-length-browser" "^4.0.0" + "@smithy/util-body-length-node" "^4.0.0" + "@smithy/util-defaults-mode-browser" "^4.0.8" + "@smithy/util-defaults-mode-node" "^4.0.8" + "@smithy/util-endpoints" "^3.0.2" + "@smithy/util-middleware" "^4.0.2" + "@smithy/util-retry" "^4.0.2" + "@smithy/util-utf8" "^4.0.0" + tslib "^2.6.2" + "@aws-sdk/region-config-resolver@3.734.0": version "3.734.0" resolved "https://registry.yarnpkg.com/@aws-sdk/region-config-resolver/-/region-config-resolver-3.734.0.tgz#45ffbc56a3e94cc5c9e0cd596b0fda60f100f70b" @@ -811,6 +1135,44 @@ "@smithy/util-middleware" "^4.0.1" tslib "^2.6.2" +"@aws-sdk/region-config-resolver@3.775.0": + version "3.775.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/region-config-resolver/-/region-config-resolver-3.775.0.tgz#592b52498e68501fe46480be3dfb185e949d1eab" + integrity sha512-40iH3LJjrQS3LKUJAl7Wj0bln7RFPEvUYKFxtP8a+oKFDO0F65F52xZxIJbPn6sHkxWDAnZlGgdjZXM3p2g5wQ== + dependencies: + "@aws-sdk/types" "3.775.0" + "@smithy/node-config-provider" "^4.0.2" + "@smithy/types" "^4.2.0" + "@smithy/util-config-provider" "^4.0.0" + "@smithy/util-middleware" "^4.0.2" + tslib "^2.6.2" + +"@aws-sdk/signature-v4-crt@^3.787.0": + version "3.787.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-crt/-/signature-v4-crt-3.787.0.tgz#6932f0f437536fc528d4cc45a79daf08819d64a3" + integrity sha512-TATbx7B/54UIyLawAM0eTkQfnfn9KlEXV1jymniEHQtsfL68VND9/uFdOp51Ob9eTo5Q3qghH0RMHZaOpRVuGA== + dependencies: + "@aws-sdk/crt-loader" "3.787.0" + "@aws-sdk/signature-v4-multi-region" "3.775.0" + "@aws-sdk/types" "3.775.0" + "@smithy/querystring-parser" "^4.0.2" + "@smithy/signature-v4" "^5.0.2" + "@smithy/types" "^4.2.0" + "@smithy/util-middleware" "^4.0.2" + tslib "^2.6.2" + +"@aws-sdk/signature-v4-multi-region@3.775.0": + version "3.775.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.775.0.tgz#80cf60f3c9a9ea00f86529f2c4497a8ce936960a" + integrity sha512-cnGk8GDfTMJ8p7+qSk92QlIk2bmTmFJqhYxcXZ9PysjZtx0xmfCMxnG3Hjy1oU2mt5boPCVSOptqtWixayM17g== + dependencies: + "@aws-sdk/middleware-sdk-s3" "3.775.0" + "@aws-sdk/types" "3.775.0" + "@smithy/protocol-http" "^5.1.0" + "@smithy/signature-v4" "^5.0.2" + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@aws-sdk/token-providers@3.734.0": version "3.734.0" resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.734.0.tgz#8880e94f21457fe5dd7074ecc52fdd43180cbb2c" @@ -835,6 +1197,18 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@aws-sdk/token-providers@3.787.0": + version "3.787.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.787.0.tgz#18c761fb21ee25c8c3a35703876f0c733b4ae743" + integrity sha512-d7/NIqxq308Zg0RPMNrmn0QvzniL4Hx8Qdwzr6YZWLYAbUSvZYS2ppLR3BFWSkV6SsTJUx8BuDaj3P8vttkrog== + dependencies: + "@aws-sdk/nested-clients" "3.787.0" + "@aws-sdk/types" "3.775.0" + "@smithy/property-provider" "^4.0.2" + "@smithy/shared-ini-file-loader" "^4.0.2" + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@aws-sdk/types@3.734.0", "@aws-sdk/types@^3.222.0": version "3.734.0" resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.734.0.tgz#af5e620b0e761918282aa1c8e53cac6091d169a2" @@ -843,6 +1217,21 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@aws-sdk/types@3.775.0": + version "3.775.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.775.0.tgz#09863a9e68c080947db7c3d226d1c56b8f0f5150" + integrity sha512-ZoGKwa4C9fC9Av6bdfqcW6Ix5ot05F/S4VxWR2nHuMv7hzfmAjTOcUiWT7UR4hM/U0whf84VhDtXN/DWAk52KA== + dependencies: + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + +"@aws-sdk/util-arn-parser@3.723.0": + version "3.723.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-arn-parser/-/util-arn-parser-3.723.0.tgz#e9bff2b13918a92d60e0012101dad60ed7db292c" + integrity sha512-ZhEfvUwNliOQROcAk34WJWVYTlTa4694kSVhDSjW6lE1bMataPnIN8A0ycukEzBXmd8ZSoBcQLn6lKGl7XIJ5w== + dependencies: + tslib "^2.6.2" + "@aws-sdk/util-dynamodb@^3.624.0": version "3.741.0" resolved "https://registry.yarnpkg.com/@aws-sdk/util-dynamodb/-/util-dynamodb-3.741.0.tgz#4d40b6120617d9cc919eff4261dff0e47757e7bd" @@ -870,6 +1259,16 @@ "@smithy/util-endpoints" "^3.0.1" tslib "^2.6.2" +"@aws-sdk/util-endpoints@3.787.0": + version "3.787.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.787.0.tgz#1398f0bd87f19e615ae920c73e16d9d5e5cb76d1" + integrity sha512-fd3zkiOkwnbdbN0Xp9TsP5SWrmv0SpT70YEdbb8wAj2DWQwiCmFszaSs+YCvhoCdmlR3Wl9Spu0pGpSAGKeYvQ== + dependencies: + "@aws-sdk/types" "3.775.0" + "@smithy/types" "^4.2.0" + "@smithy/util-endpoints" "^3.0.2" + tslib "^2.6.2" + "@aws-sdk/util-locate-window@^3.0.0": version "3.723.0" resolved "https://registry.yarnpkg.com/@aws-sdk/util-locate-window/-/util-locate-window-3.723.0.tgz#174551bfdd2eb36d3c16e7023fd7e7ee96ad0fa9" @@ -887,6 +1286,16 @@ bowser "^2.11.0" tslib "^2.6.2" +"@aws-sdk/util-user-agent-browser@3.775.0": + version "3.775.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.775.0.tgz#b69a1a5548ccc6db1acb3ec115967593ece927a1" + integrity sha512-txw2wkiJmZKVdDbscK7VBK+u+TJnRtlUjRTLei+elZg2ADhpQxfVAQl436FUeIv6AhB/oRHW6/K/EAGXUSWi0A== + dependencies: + "@aws-sdk/types" "3.775.0" + "@smithy/types" "^4.2.0" + bowser "^2.11.0" + tslib "^2.6.2" + "@aws-sdk/util-user-agent-node@3.734.0": version "3.734.0" resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.734.0.tgz#d5c6ee192cea9d53a871178a2669b8b4dea39a68" @@ -909,6 +1318,24 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@aws-sdk/util-user-agent-node@3.787.0": + version "3.787.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.787.0.tgz#58e63e99586cde1c1314f74b94596780321442f5" + integrity sha512-mG7Lz8ydfG4SF9e8WSXiPQ/Lsn3n8A5B5jtPROidafi06I3ckV2WxyMLdwG14m919NoS6IOfWHyRGSqWIwbVKA== + dependencies: + "@aws-sdk/middleware-user-agent" "3.787.0" + "@aws-sdk/types" "3.775.0" + "@smithy/node-config-provider" "^4.0.2" + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + +"@aws-sdk/util-utf8-browser@^3.259.0": + version "3.259.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz#3275a6f5eb334f96ca76635b961d3c50259fd9ff" + integrity sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw== + dependencies: + tslib "^2.3.1" + "@azure/msal-browser@^3.20.0": version "3.28.1" resolved "https://registry.yarnpkg.com/@azure/msal-browser/-/msal-browser-3.28.1.tgz#9132fc8807bfcc2b1c3b3c3b9a85d4df41457148" @@ -918,7 +1345,7 @@ "@azure/msal-common@14.16.0": version "14.16.0" - resolved "https://registry.yarnpkg.com/@azure/msal-common/-/msal-common-14.16.0.tgz#f3470fcaec788dbe50859952cd499340bda23d7a" + resolved "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.0.tgz" integrity sha512-1KOZj9IpcDSwpNiQNjt0jDYZpQvNZay7QAEi/5DLubay40iGYtLzya/jbjRPLyOTZhEKyL1MzPuw2HqBCjceYA== "@azure/msal-node@^2.16.1": @@ -1225,7 +1652,7 @@ "@discordjs/collection@1.5.3": version "1.5.3" - resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-1.5.3.tgz#5a1250159ebfff9efa4f963cfa7e97f1b291be18" + resolved "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz" integrity sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ== "@discordjs/collection@^2.1.0", "@discordjs/collection@^2.1.1": @@ -1342,7 +1769,7 @@ "@esbuild/darwin-arm64@0.21.5": version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" + resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz" integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== "@esbuild/darwin-arm64@0.23.1": @@ -1659,7 +2086,7 @@ "@eslint/eslintrc@^2.1.4": version "2.1.4" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz" integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== dependencies: ajv "^6.12.4" @@ -1723,7 +2150,7 @@ "@fastify/error@^4.0.0": version "4.0.0" - resolved "https://registry.yarnpkg.com/@fastify/error/-/error-4.0.0.tgz#7842d6161fbce78953638318be99033a0c2d5070" + resolved "https://registry.npmjs.org/@fastify/error/-/error-4.0.0.tgz" integrity sha512-OO/SA8As24JtT1usTUTKgGH7uLvhfwZPwlptRi2Dp5P4KKmJI3gvsZ8MIHnNwDs4sLf/aai5LzTyl66xr7qMxA== "@fastify/fast-json-stringify-compiler@^5.0.0": @@ -1764,7 +2191,7 @@ http-errors "^2.0.0" mime "^3" -"@fastify/static@^8.1.1": +"@fastify/static@^8.0.0", "@fastify/static@^8.1.1": version "8.1.1" resolved "https://registry.yarnpkg.com/@fastify/static/-/static-8.1.1.tgz#406bfab6b9c5d9ccb0f6b41e66963d6775c11ead" integrity sha512-TW9eyVHJLytZNpBlSIqd0bl1giJkEaRaPZG+5AT3L/OBKq9U8D7g/OYmc2NPQZnzPURGhMt3IAWuyVkvd2nOkQ== @@ -1776,6 +2203,28 @@ fastq "^1.17.1" glob "^11.0.0" +"@fastify/swagger-ui@^5.2.2": + version "5.2.2" + resolved "https://registry.yarnpkg.com/@fastify/swagger-ui/-/swagger-ui-5.2.2.tgz#98d6856ae1795c5e53fdc8374235b0df425aa972" + integrity sha512-jf8xe+D8Xjc8TqrZhtlJImOWihd8iYFu8dhM01mGg+F04CKUM0zGB9aADE9nxzRUszyWp3wn+uWk89nbAoBMCw== + dependencies: + "@fastify/static" "^8.0.0" + fastify-plugin "^5.0.0" + openapi-types "^12.1.3" + rfdc "^1.3.1" + yaml "^2.4.1" + +"@fastify/swagger@^9.5.0": + version "9.5.0" + resolved "https://registry.yarnpkg.com/@fastify/swagger/-/swagger-9.5.0.tgz#761ce5aae9e76116cd1b6fee4b463c7d1d11f4a7" + integrity sha512-6WiwB1Nh+GHqm4wsDGH/ym6ming3DyH9cuAkIwGN9nhbyrCoNSZ+l9h3TAsksffbNEI/RHCiw2BH2LeGNRrOoQ== + dependencies: + fastify-plugin "^5.0.0" + json-schema-resolver "^3.0.0" + openapi-types "^12.1.3" + rfdc "^1.3.1" + yaml "^2.4.2" + "@floating-ui/core@^1.6.0": version "1.6.9" resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.9.tgz#64d1da251433019dafa091de9b2886ff35ec14e6" @@ -1824,6 +2273,20 @@ dependencies: "@hapi/hoek" "^9.0.0" +"@httptoolkit/websocket-stream@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@httptoolkit/websocket-stream/-/websocket-stream-6.0.1.tgz#8d732f1509860236276f6b0759db4cc9859bbb62" + integrity sha512-A0NOZI+Glp3Xgcz6Na7i7o09+/+xm2m0UCU8gdtM2nIv6/cjLmhMZMqehSpTlgbx9omtLmV8LVqOskPEyWnmZQ== + dependencies: + "@types/ws" "*" + duplexify "^3.5.1" + inherits "^2.0.1" + isomorphic-ws "^4.0.1" + readable-stream "^2.3.3" + safe-buffer "^5.1.2" + ws "*" + xtend "^4.0.0" + "@humanwhocodes/config-array@^0.13.0": version "0.13.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" @@ -1835,7 +2298,7 @@ "@humanwhocodes/module-importer@^1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== "@humanwhocodes/object-schema@^2.0.3": @@ -1879,22 +2342,22 @@ "@jridgewell/resolve-uri@^3.1.0": version "3.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== "@jridgewell/set-array@^1.2.1": version "1.2.1" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz" integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.13", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": version "1.5.0" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz" integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== "@jridgewell/trace-mapping@^0.3.23", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": version "0.3.25" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz" integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== dependencies: "@jridgewell/resolve-uri" "^3.1.0" @@ -1986,75 +2449,75 @@ resolved "https://registry.yarnpkg.com/@middy/util/-/util-6.0.0.tgz#dddada0cfa40dcdfc0b41bd116a58ae14a3212a8" integrity sha512-V2/gJ4wE6TtMJNAnUTm3VRdgNyLI6zdNLy3MzhrJOwxiUslG1OSShE1IUYR0cmzMOm5w/Y2p3+OIRXRqKUVHYQ== -"@napi-rs/canvas-android-arm64@0.1.66": - version "0.1.66" - resolved "https://registry.yarnpkg.com/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.66.tgz#c5205cecc525502799690d01f821c4156c1c2dc0" - integrity sha512-77Yq9yaUYN90zCovYOpw7LhidJiswU9wLIWWBGF6iiEJyQdt6tkiXpGRZpOMJVO70afkcdc4T7532cxMIBhk0Q== - -"@napi-rs/canvas-darwin-arm64@0.1.66": - version "0.1.66" - resolved "https://registry.yarnpkg.com/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.66.tgz#e619321dd611cceaa1f75d018881f03b01aa21d7" - integrity sha512-cz3aJ06b8BZGtwRxKTiE0OVUlB17MH8j+BnE4A5+wD9aD1guCCqECsz+k7tpXdAdTAYKRIz2pq6ZuiJ76NyUbQ== - -"@napi-rs/canvas-darwin-x64@0.1.66": - version "0.1.66" - resolved "https://registry.yarnpkg.com/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.66.tgz#e39d4ac4d427c698a074609d85d8e6b1d53a2e16" - integrity sha512-szIWqJgFm2OTyGzM+hSiJOaOtjI73VYRC2KN30zZTt7i1+0sgpm5exK5ltDBPOmCdnLt7SbUfpInLj8VvxYlKA== - -"@napi-rs/canvas-linux-arm-gnueabihf@0.1.66": - version "0.1.66" - resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.66.tgz#4db919baa52b21700fad1beaaaf100f25c87505b" - integrity sha512-h/TZJFc6JLvp8FwbA5mu+yXiblN0iKqshU7xzd6L+ks5uNYgjS7XWLkNiyPQkMaXQgVczOJfZy7r4NSPK3V8Hg== - -"@napi-rs/canvas-linux-arm64-gnu@0.1.66": - version "0.1.66" - resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.66.tgz#815bd721ad44e8133930a7ded016cf855833031c" - integrity sha512-RGFUdBdi0Xmf+TfwZcB89Ap6hDYh4nzyJhXhNJIgve6ELrIPFhf7sDHvUHxjgW0YzczGoo+ophyCm03cJggu+w== - -"@napi-rs/canvas-linux-arm64-musl@0.1.66": - version "0.1.66" - resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.66.tgz#0e95d8f3cb58cdbba5c8b99ddc03506476694cfb" - integrity sha512-2cFViDIZ0xQlAHyJmyym+rj3p04V16vgAiz64sCAfwOOiW6e19agv1HQWHUsro3G2lF3PaHGAnp0WRPXGqLOfg== - -"@napi-rs/canvas-linux-riscv64-gnu@0.1.66": - version "0.1.66" - resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.66.tgz#9b01db989a66b06981ed19b3d67e3f27cb74d2a9" - integrity sha512-Vm5ZWS2RDPeBpnfx83eJpZfJT07xl0jqp8d83PklKqiDNa3BmDZZ/uuI40/ICgejGLymXXYo5N21b7oAxhRTSA== - -"@napi-rs/canvas-linux-x64-gnu@0.1.66": - version "0.1.66" - resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.66.tgz#a2f0ba9fee3bcd769c25a22e85863a6c90c34913" - integrity sha512-/ptGBhErNBCgWff3khtuEjhiiYWf70oWvBPRj8y5EMB0nLYpve7RxxFnavVvxN49kJ0MQHRIwgfyd47RSOOKPw== - -"@napi-rs/canvas-linux-x64-musl@0.1.66": - version "0.1.66" - resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.66.tgz#44703f1deb47b6040c14edec952a4711ad52672c" - integrity sha512-XunvXisTkIG+bpq6BcXmsUstoLX3RLS6N9Uz9Pg9RpWIMeM6ObR5shr3NgpGRJq93769I1hS4mJW0DX2Au3WBw== - -"@napi-rs/canvas-win32-x64-msvc@0.1.66": - version "0.1.66" - resolved "https://registry.yarnpkg.com/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.66.tgz#a14e48c2173eb3b445da7eaf4f6a33bac8825ee4" - integrity sha512-3n34watNFqpwACDA+pt4jfQD6zR8PzfK86FBajdsgDVVZhSp6ohgbbJv+eUrXM08VUtjxTq7+U4sWspTu9+4Ug== +"@napi-rs/canvas-android-arm64@0.1.69": + version "0.1.69" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.69.tgz#0ce738f90b3532d43505eebb650cb4763c24ea66" + integrity sha512-4icWTByY8zPvM9SelfQKf3I6kwXw0aI5drBOVrwfER5kjwXJd78FPSDSZkxDHjvIo9Q86ljl18Yr963ehA4sHQ== + +"@napi-rs/canvas-darwin-arm64@0.1.69": + version "0.1.69" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.69.tgz#826a2b45af433a4aea8c0e0edfe1ca1fcfd5b9e9" + integrity sha512-HOanhhYlHdukA+unjelT4Dg3ta7e820x87/AG2dKUMsUzH19jaeZs9bcYjzEy2vYi/dFWKz7cSv2yaIOudB8Yg== + +"@napi-rs/canvas-darwin-x64@0.1.69": + version "0.1.69" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.69.tgz#a78b3e8c44d6a93e36a8ef1676e96712c5ad11be" + integrity sha512-SIp7WfhxAPnSVK9bkFfJp+84rbATCIq9jMUzDwpCLhQ+v+OqtXe4pggX1oeV+62/HK6BT1t18qRmJfyqwJ9f3g== + +"@napi-rs/canvas-linux-arm-gnueabihf@0.1.69": + version "0.1.69" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.69.tgz#47369309e87e047335379bea628625b740c7ab2b" + integrity sha512-Ls+KujCp6TGpkuMVFvrlx+CxtL+casdkrprFjqIuOAnB30Mct6bCEr+I83Tu29s3nNq4EzIGjdmA3fFAZG/Dtw== + +"@napi-rs/canvas-linux-arm64-gnu@0.1.69": + version "0.1.69" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.69.tgz#1f23dd74a0f53db95309a58f989cf6be1bb6363e" + integrity sha512-m8VcGmeSBNRbHZBd1srvdM1aq/ScS2y8KqGqmCCEgJlytYK4jdULzAo2K/BPKE1v3xvn8oUPZDLI/NBJbJkEoA== + +"@napi-rs/canvas-linux-arm64-musl@0.1.69": + version "0.1.69" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.69.tgz#ed719b19acf60888ef62a40951b9c51891bd9fcf" + integrity sha512-a3xjNRIeK2m2ZORGv2moBvv3vbkaFZG1QKMeiEv/BKij+rkztuEhTJGMar+buICFgS0fLgphXXsKNkUSJb7eRQ== + +"@napi-rs/canvas-linux-riscv64-gnu@0.1.69": + version "0.1.69" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.69.tgz#444993e0b6fc52cb7cbc838e25df2d62b5fb17b0" + integrity sha512-pClUoJF5wdC9AvD0mc15G9JffL1Q85nuH1rLSQPRkGmGmQOtRjw5E9xNbanz7oFUiPbjH7xcAXUjVAcf7tdgPQ== + +"@napi-rs/canvas-linux-x64-gnu@0.1.69": + version "0.1.69" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.69.tgz#87afd2f8ba1b442b45429c03d5aa98eb9872772f" + integrity sha512-96X3bFAmzemfw84Ts6Jg/omL86uuynvK06MWGR/mp3JYNumY9RXofA14eF/kJIYelbYFWXcwpbcBR71lJ6G/YQ== + +"@napi-rs/canvas-linux-x64-musl@0.1.69": + version "0.1.69" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.69.tgz#4f743a8d649f61dc121f03b6f40dff2e5d318aac" + integrity sha512-2QTsEFO72Kwkj53W9hc5y1FAUvdGx0V+pjJB+9oQF6Ys9+y989GyPIl5wZDzeh8nIJW6koZZ1eFa8pD+pA5BFQ== + +"@napi-rs/canvas-win32-x64-msvc@0.1.69": + version "0.1.69" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.69.tgz#7b63c1694c5ef11db9695f5917e3f6670562bc60" + integrity sha512-Q4YA8kVnKarApBVLu7F8icGlIfSll5Glswo5hY6gPS4Is2dCI8+ig9OeDM8RlwYevUIxKq8lZBypN8Q1iLAQ7w== "@napi-rs/canvas@^0.1.65": - version "0.1.66" - resolved "https://registry.yarnpkg.com/@napi-rs/canvas/-/canvas-0.1.66.tgz#71c9dff25a89fc8aadd1e65b9ae4ef4500ffd5b8" - integrity sha512-NE/eQKLbUS+LCbMHRa5HnR7cc1Q4ibg/qfLUN4Ukl3CC0lq6LfHE0YbvFm/l4i5RyyS+aUjL+8IuZDD9EH3amg== + version "0.1.69" + resolved "https://registry.yarnpkg.com/@napi-rs/canvas/-/canvas-0.1.69.tgz#ac2e4113d0bd381568219d1ba22f0a8563d4c6b1" + integrity sha512-ydvNeJMRm+l3T14yCoUKqjYQiEdXDq1isznI93LEBGYssXKfSaLNLHOkeM4z9Fnw9Pkt2EKOCAtW9cS4b00Zcg== optionalDependencies: - "@napi-rs/canvas-android-arm64" "0.1.66" - "@napi-rs/canvas-darwin-arm64" "0.1.66" - "@napi-rs/canvas-darwin-x64" "0.1.66" - "@napi-rs/canvas-linux-arm-gnueabihf" "0.1.66" - "@napi-rs/canvas-linux-arm64-gnu" "0.1.66" - "@napi-rs/canvas-linux-arm64-musl" "0.1.66" - "@napi-rs/canvas-linux-riscv64-gnu" "0.1.66" - "@napi-rs/canvas-linux-x64-gnu" "0.1.66" - "@napi-rs/canvas-linux-x64-musl" "0.1.66" - "@napi-rs/canvas-win32-x64-msvc" "0.1.66" + "@napi-rs/canvas-android-arm64" "0.1.69" + "@napi-rs/canvas-darwin-arm64" "0.1.69" + "@napi-rs/canvas-darwin-x64" "0.1.69" + "@napi-rs/canvas-linux-arm-gnueabihf" "0.1.69" + "@napi-rs/canvas-linux-arm64-gnu" "0.1.69" + "@napi-rs/canvas-linux-arm64-musl" "0.1.69" + "@napi-rs/canvas-linux-riscv64-gnu" "0.1.69" + "@napi-rs/canvas-linux-x64-gnu" "0.1.69" + "@napi-rs/canvas-linux-x64-musl" "0.1.69" + "@napi-rs/canvas-win32-x64-msvc" "0.1.69" "@nodelib/fs.scandir@2.1.5": version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== dependencies: "@nodelib/fs.stat" "2.0.5" @@ -2062,12 +2525,12 @@ "@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== "@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== dependencies: "@nodelib/fs.scandir" "2.1.5" @@ -2078,6 +2541,11 @@ resolved "https://registry.yarnpkg.com/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz#3dc35ba0f1e66b403c00b39344f870298ebb1c8e" integrity sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA== +"@phc/format@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@phc/format/-/format-1.0.0.tgz#b5627003b3216dc4362125b13f48a4daa76680e4" + integrity sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ== + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -2085,7 +2553,7 @@ "@pkgr/core@^0.1.0": version "0.1.1" - resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" + resolved "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz" integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== "@playwright/test@^1.49.1": @@ -2229,7 +2697,7 @@ "@sapphire/snowflake@3.5.3": version "3.5.3" - resolved "https://registry.yarnpkg.com/@sapphire/snowflake/-/snowflake-3.5.3.tgz#0c102aa2ec5b34f806e9bc8625fc6a5e1d0a0c6a" + resolved "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz" integrity sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ== "@sapphire/snowflake@^3.5.3": @@ -2256,7 +2724,7 @@ "@sinonjs/commons@^3.0.0", "@sinonjs/commons@^3.0.1": version "3.0.1" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" + resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz" integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== dependencies: type-detect "4.0.8" @@ -2297,6 +2765,14 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@smithy/abort-controller@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@smithy/abort-controller/-/abort-controller-4.0.2.tgz#36a23e8cc65fc03cacb6afa35dfbfd319c560c6b" + integrity sha512-Sl/78VDtgqKxN2+1qduaVE140XF+Xg+TafkncspwM4jFP/LHr76ZHmIY/y3V1M0mMLNk+Je6IGbzxy23RSToMw== + dependencies: + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@smithy/config-resolver@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@smithy/config-resolver/-/config-resolver-4.0.1.tgz#3d6c78bbc51adf99c9819bb3f0ea197fe03ad363" @@ -2308,6 +2784,17 @@ "@smithy/util-middleware" "^4.0.1" tslib "^2.6.2" +"@smithy/config-resolver@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@smithy/config-resolver/-/config-resolver-4.1.0.tgz#de1043cbd75f05d99798b0fbcfdaf4b89b0f2f41" + integrity sha512-8smPlwhga22pwl23fM5ew4T9vfLUCeFXlcqNOCD5M5h8VmNPNUE9j6bQSuRXpDSV11L/E/SwEBQuW8hr6+nS1A== + dependencies: + "@smithy/node-config-provider" "^4.0.2" + "@smithy/types" "^4.2.0" + "@smithy/util-config-provider" "^4.0.0" + "@smithy/util-middleware" "^4.0.2" + tslib "^2.6.2" + "@smithy/core@^3.1.1", "@smithy/core@^3.1.2": version "3.1.2" resolved "https://registry.yarnpkg.com/@smithy/core/-/core-3.1.2.tgz#f5b4c89bf054b717781d71c66b4fb594e06cbb62" @@ -2336,6 +2823,20 @@ "@smithy/util-utf8" "^4.0.0" tslib "^2.6.2" +"@smithy/core@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@smithy/core/-/core-3.2.0.tgz#613b15f76eab9a6be396b1d5453b6bc8f22ba99c" + integrity sha512-k17bgQhVZ7YmUvA8at4af1TDpl0NDMBuBKJl8Yg0nrefwmValU+CnA5l/AriVdQNthU/33H3nK71HrLgqOPr1Q== + dependencies: + "@smithy/middleware-serde" "^4.0.3" + "@smithy/protocol-http" "^5.1.0" + "@smithy/types" "^4.2.0" + "@smithy/util-body-length-browser" "^4.0.0" + "@smithy/util-middleware" "^4.0.2" + "@smithy/util-stream" "^4.2.0" + "@smithy/util-utf8" "^4.0.0" + tslib "^2.6.2" + "@smithy/credential-provider-imds@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.1.tgz#807110739982acd1588a4847b61e6edf196d004e" @@ -2347,6 +2848,17 @@ "@smithy/url-parser" "^4.0.1" tslib "^2.6.2" +"@smithy/credential-provider-imds@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.2.tgz#1ec34a04842fa69996b151a695b027f0486c69a8" + integrity sha512-32lVig6jCaWBHnY+OEQ6e6Vnt5vDHaLiydGrwYMW9tPqO688hPGTYRamYJ1EptxEC2rAwJrHWmPoKRBl4iTa8w== + dependencies: + "@smithy/node-config-provider" "^4.0.2" + "@smithy/property-provider" "^4.0.2" + "@smithy/types" "^4.2.0" + "@smithy/url-parser" "^4.0.2" + tslib "^2.6.2" + "@smithy/fetch-http-handler@^5.0.1": version "5.0.1" resolved "https://registry.yarnpkg.com/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.1.tgz#8463393442ca6a1644204849e42c386066f0df79" @@ -2358,6 +2870,17 @@ "@smithy/util-base64" "^4.0.0" tslib "^2.6.2" +"@smithy/fetch-http-handler@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.2.tgz#9d3cacf044aa9573ab933f445ab95cddb284813d" + integrity sha512-+9Dz8sakS9pe7f2cBocpJXdeVjMopUDLgZs1yWeu7h++WqSbjUYv/JAJwKwXw1HV6gq1jyWjxuyn24E2GhoEcQ== + dependencies: + "@smithy/protocol-http" "^5.1.0" + "@smithy/querystring-builder" "^4.0.2" + "@smithy/types" "^4.2.0" + "@smithy/util-base64" "^4.0.0" + tslib "^2.6.2" + "@smithy/hash-node@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@smithy/hash-node/-/hash-node-4.0.1.tgz#ce78fc11b848a4f47c2e1e7a07fb6b982d2f130c" @@ -2368,6 +2891,16 @@ "@smithy/util-utf8" "^4.0.0" tslib "^2.6.2" +"@smithy/hash-node@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@smithy/hash-node/-/hash-node-4.0.2.tgz#a34fe5a33b067d754ca63302b9791778f003e437" + integrity sha512-VnTpYPnRUE7yVhWozFdlxcYknv9UN7CeOqSrMH+V877v4oqtVYuoqhIhtSjmGPvYrYnAkaM61sLMKHvxL138yg== + dependencies: + "@smithy/types" "^4.2.0" + "@smithy/util-buffer-from" "^4.0.0" + "@smithy/util-utf8" "^4.0.0" + tslib "^2.6.2" + "@smithy/invalid-dependency@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@smithy/invalid-dependency/-/invalid-dependency-4.0.1.tgz#704d1acb6fac105558c17d53f6d55da6b0d6b6fc" @@ -2376,9 +2909,17 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@smithy/invalid-dependency@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@smithy/invalid-dependency/-/invalid-dependency-4.0.2.tgz#e9b1c5e407d795f10a03afba90e37bccdc3e38f7" + integrity sha512-GatB4+2DTpgWPday+mnUkoumP54u/MDM/5u44KF9hIu8jF0uafZtQLcdfIKkIcUNuF/fBojpLEHZS/56JqPeXQ== + dependencies: + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@smithy/is-array-buffer@^2.2.0": version "2.2.0" - resolved "https://registry.yarnpkg.com/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz#f84f0d9f9a36601a9ca9381688bd1b726fd39111" + resolved "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz" integrity sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA== dependencies: tslib "^2.6.2" @@ -2408,6 +2949,15 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@smithy/middleware-content-length@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@smithy/middleware-content-length/-/middleware-content-length-4.0.2.tgz#ff78658e8047ad7038f58478cf8713ee2f6ef647" + integrity sha512-hAfEXm1zU+ELvucxqQ7I8SszwQ4znWMbNv6PLMndN83JJN41EPuS93AIyh2N+gJ6x8QFhzSO6b7q2e6oClDI8A== + dependencies: + "@smithy/protocol-http" "^5.1.0" + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@smithy/middleware-endpoint@^4.0.2", "@smithy/middleware-endpoint@^4.0.3": version "4.0.3" resolved "https://registry.yarnpkg.com/@smithy/middleware-endpoint/-/middleware-endpoint-4.0.3.tgz#74b64fb2473ae35649a8d22d41708bc5d8d99df2" @@ -2436,6 +2986,20 @@ "@smithy/util-middleware" "^4.0.1" tslib "^2.6.2" +"@smithy/middleware-endpoint@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.0.tgz#cbfe47c5632942c960dbcf71fb02fd0d9985444d" + integrity sha512-xhLimgNCbCzsUppRTGXWkZywksuTThxaIB0HwbpsVLY5sceac4e1TZ/WKYqufQLaUy+gUSJGNdwD2jo3cXL0iA== + dependencies: + "@smithy/core" "^3.2.0" + "@smithy/middleware-serde" "^4.0.3" + "@smithy/node-config-provider" "^4.0.2" + "@smithy/shared-ini-file-loader" "^4.0.2" + "@smithy/types" "^4.2.0" + "@smithy/url-parser" "^4.0.2" + "@smithy/util-middleware" "^4.0.2" + tslib "^2.6.2" + "@smithy/middleware-retry@^4.0.3": version "4.0.4" resolved "https://registry.yarnpkg.com/@smithy/middleware-retry/-/middleware-retry-4.0.4.tgz#95e55a1b163ff06264f20b4dbbcbd915c8028f60" @@ -2466,6 +3030,21 @@ tslib "^2.6.2" uuid "^9.0.1" +"@smithy/middleware-retry@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@smithy/middleware-retry/-/middleware-retry-4.1.0.tgz#338ac1e025bbc6fd7b008152c4efa8bc0591acc9" + integrity sha512-2zAagd1s6hAaI/ap6SXi5T3dDwBOczOMCSkkYzktqN1+tzbk1GAsHNAdo/1uzxz3Ky02jvZQwbi/vmDA6z4Oyg== + dependencies: + "@smithy/node-config-provider" "^4.0.2" + "@smithy/protocol-http" "^5.1.0" + "@smithy/service-error-classification" "^4.0.2" + "@smithy/smithy-client" "^4.2.0" + "@smithy/types" "^4.2.0" + "@smithy/util-middleware" "^4.0.2" + "@smithy/util-retry" "^4.0.2" + tslib "^2.6.2" + uuid "^9.0.1" + "@smithy/middleware-serde@^4.0.1", "@smithy/middleware-serde@^4.0.2": version "4.0.2" resolved "https://registry.yarnpkg.com/@smithy/middleware-serde/-/middleware-serde-4.0.2.tgz#f792d72f6ad8fa6b172e3f19c6fe1932a856a56d" @@ -2474,6 +3053,14 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@smithy/middleware-serde@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@smithy/middleware-serde/-/middleware-serde-4.0.3.tgz#b90ef1065ad9dc0b54c561fae73c8a5792d145e3" + integrity sha512-rfgDVrgLEVMmMn0BI8O+8OVr6vXzjV7HZj57l0QxslhzbvVfikZbVfBVthjLHqib4BW44QhcIgJpvebHlRaC9A== + dependencies: + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@smithy/middleware-stack@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@smithy/middleware-stack/-/middleware-stack-4.0.1.tgz#c157653f9df07f7c26e32f49994d368e4e071d22" @@ -2482,6 +3069,14 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@smithy/middleware-stack@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@smithy/middleware-stack/-/middleware-stack-4.0.2.tgz#ca7bc3eedc7c1349e2cf94e0dc92a68d681bef18" + integrity sha512-eSPVcuJJGVYrFYu2hEq8g8WWdJav3sdrI4o2c6z/rjnYDd3xH9j9E7deZQCzFn4QvGPouLngH3dQ+QVTxv5bOQ== + dependencies: + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@smithy/node-config-provider@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@smithy/node-config-provider/-/node-config-provider-4.0.1.tgz#4e84fe665c0774d5f4ebb75144994fc6ebedf86e" @@ -2492,6 +3087,16 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@smithy/node-config-provider@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@smithy/node-config-provider/-/node-config-provider-4.0.2.tgz#017ba626828bced0fa588e795246e5468632f3ef" + integrity sha512-WgCkILRZfJwJ4Da92a6t3ozN/zcvYyJGUTmfGbgS/FkCcoCjl7G4FJaCDN1ySdvLvemnQeo25FdkyMSTSwulsw== + dependencies: + "@smithy/property-provider" "^4.0.2" + "@smithy/shared-ini-file-loader" "^4.0.2" + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@smithy/node-http-handler@^4.0.2": version "4.0.2" resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-4.0.2.tgz#48d47a046cf900ab86bfbe7f5fd078b52c82fab6" @@ -2514,6 +3119,17 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@smithy/node-http-handler@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-4.0.4.tgz#aa583d201c1ee968170b65a07f06d633c214b7a1" + integrity sha512-/mdqabuAT3o/ihBGjL94PUbTSPSRJ0eeVTdgADzow0wRJ0rN4A27EOrtlK56MYiO1fDvlO3jVTCxQtQmK9dZ1g== + dependencies: + "@smithy/abort-controller" "^4.0.2" + "@smithy/protocol-http" "^5.1.0" + "@smithy/querystring-builder" "^4.0.2" + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@smithy/property-provider@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@smithy/property-provider/-/property-provider-4.0.1.tgz#8d35d5997af2a17cf15c5e921201ef6c5e3fc870" @@ -2522,6 +3138,14 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@smithy/property-provider@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@smithy/property-provider/-/property-provider-4.0.2.tgz#4572c10415c9d4215f3df1530ba61b0319b17b55" + integrity sha512-wNRoQC1uISOuNc2s4hkOYwYllmiyrvVXWMtq+TysNRVQaHm4yoafYQyjN/goYZS+QbYlPIbb/QRjaUZMuzwQ7A== + dependencies: + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@smithy/protocol-http@^5.0.1": version "5.0.1" resolved "https://registry.yarnpkg.com/@smithy/protocol-http/-/protocol-http-5.0.1.tgz#37c248117b29c057a9adfad4eb1d822a67079ff1" @@ -2530,6 +3154,14 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@smithy/protocol-http@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@smithy/protocol-http/-/protocol-http-5.1.0.tgz#ad34e336a95944785185234bebe2ec8dbe266936" + integrity sha512-KxAOL1nUNw2JTYrtviRRjEnykIDhxc84qMBzxvu1MUfQfHTuBlCG7PA6EdVwqpJjH7glw7FqQoFxUJSyBQgu7g== + dependencies: + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@smithy/querystring-builder@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@smithy/querystring-builder/-/querystring-builder-4.0.1.tgz#37e1e05d0d33c6f694088abc3e04eafb65cb6976" @@ -2539,6 +3171,15 @@ "@smithy/util-uri-escape" "^4.0.0" tslib "^2.6.2" +"@smithy/querystring-builder@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@smithy/querystring-builder/-/querystring-builder-4.0.2.tgz#834cea95bf413ab417bf9c166d60fd80d2cb3016" + integrity sha512-NTOs0FwHw1vimmQM4ebh+wFQvOwkEf/kQL6bSM1Lock+Bv4I89B3hGYoUEPkmvYPkDKyp5UdXJYu+PoTQ3T31Q== + dependencies: + "@smithy/types" "^4.2.0" + "@smithy/util-uri-escape" "^4.0.0" + tslib "^2.6.2" + "@smithy/querystring-parser@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@smithy/querystring-parser/-/querystring-parser-4.0.1.tgz#312dc62b146f8bb8a67558d82d4722bb9211af42" @@ -2547,6 +3188,14 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@smithy/querystring-parser@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@smithy/querystring-parser/-/querystring-parser-4.0.2.tgz#d80c5afb740e12ad8b4d4f58415e402c69712479" + integrity sha512-v6w8wnmZcVXjfVLjxw8qF7OwESD9wnpjp0Dqry/Pod0/5vcEA3qxCr+BhbOHlxS8O+29eLpT3aagxXGwIoEk7Q== + dependencies: + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@smithy/service-error-classification@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@smithy/service-error-classification/-/service-error-classification-4.0.1.tgz#84e78579af46c7b79c900b6d6cc822c9465f3259" @@ -2554,6 +3203,13 @@ dependencies: "@smithy/types" "^4.1.0" +"@smithy/service-error-classification@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@smithy/service-error-classification/-/service-error-classification-4.0.2.tgz#96740ed8be7ac5ad7d6f296d4ddf3f66444b8dcc" + integrity sha512-LA86xeFpTKn270Hbkixqs5n73S+LVM0/VZco8dqd+JT75Dyx3Lcw/MraL7ybjmz786+160K8rPOmhsq0SocoJQ== + dependencies: + "@smithy/types" "^4.2.0" + "@smithy/shared-ini-file-loader@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.1.tgz#d35c21c29454ca4e58914a4afdde68d3b2def1ee" @@ -2562,6 +3218,14 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@smithy/shared-ini-file-loader@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.2.tgz#15043f0516fe09ff4b22982bc5f644dc701ebae5" + integrity sha512-J9/gTWBGVuFZ01oVA6vdb4DAjf1XbDhK6sLsu3OS9qmLrS6KB5ygpeHiM3miIbj1qgSJ96GYszXFWv6ErJ8QEw== + dependencies: + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@smithy/signature-v4@^5.0.1": version "5.0.1" resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-5.0.1.tgz#f93401b176150286ba246681031b0503ec359270" @@ -2576,6 +3240,20 @@ "@smithy/util-utf8" "^4.0.0" tslib "^2.6.2" +"@smithy/signature-v4@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-5.0.2.tgz#363854e946fbc5bc206ff82e79ada5d5c14be640" + integrity sha512-Mz+mc7okA73Lyz8zQKJNyr7lIcHLiPYp0+oiqiMNc/t7/Kf2BENs5d63pEj7oPqdjaum6g0Fc8wC78dY1TgtXw== + dependencies: + "@smithy/is-array-buffer" "^4.0.0" + "@smithy/protocol-http" "^5.1.0" + "@smithy/types" "^4.2.0" + "@smithy/util-hex-encoding" "^4.0.0" + "@smithy/util-middleware" "^4.0.2" + "@smithy/util-uri-escape" "^4.0.0" + "@smithy/util-utf8" "^4.0.0" + tslib "^2.6.2" + "@smithy/smithy-client@^4.1.2", "@smithy/smithy-client@^4.1.3": version "4.1.3" resolved "https://registry.yarnpkg.com/@smithy/smithy-client/-/smithy-client-4.1.3.tgz#2c8f9aff3377e7655cebe84239da6be277ba8554" @@ -2602,6 +3280,19 @@ "@smithy/util-stream" "^4.1.2" tslib "^2.6.2" +"@smithy/smithy-client@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/smithy-client/-/smithy-client-4.2.0.tgz#0c64cae4fb5bb4f26386e9b2c33fc9a3c24c9df3" + integrity sha512-Qs65/w30pWV7LSFAez9DKy0Koaoh3iHhpcpCCJ4waj/iqwsuSzJna2+vYwq46yBaqO5ZbP9TjUsATUNxrKeBdw== + dependencies: + "@smithy/core" "^3.2.0" + "@smithy/middleware-endpoint" "^4.1.0" + "@smithy/middleware-stack" "^4.0.2" + "@smithy/protocol-http" "^5.1.0" + "@smithy/types" "^4.2.0" + "@smithy/util-stream" "^4.2.0" + tslib "^2.6.2" + "@smithy/types@^4.1.0": version "4.1.0" resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.1.0.tgz#19de0b6087bccdd4182a334eb5d3d2629699370f" @@ -2609,6 +3300,13 @@ dependencies: tslib "^2.6.2" +"@smithy/types@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.2.0.tgz#e7998984cc54b1acbc32e6d4cf982c712e3d26b6" + integrity sha512-7eMk09zQKCO+E/ivsjQv+fDlOupcFUCSC/L2YUPgwhvowVGWbPQHjEFcmjt7QQ4ra5lyowS92SV53Zc6XD4+fg== + dependencies: + tslib "^2.6.2" + "@smithy/url-parser@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@smithy/url-parser/-/url-parser-4.0.1.tgz#b47743f785f5b8d81324878cbb1b5f834bf8d85a" @@ -2618,6 +3316,15 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@smithy/url-parser@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@smithy/url-parser/-/url-parser-4.0.2.tgz#a316f7d8593ffab796348bc5df96237833880713" + integrity sha512-Bm8n3j2ScqnT+kJaClSVCMeiSenK6jVAzZCNewsYWuZtnBehEz4r2qP0riZySZVfzB+03XZHJeqfmJDkeeSLiQ== + dependencies: + "@smithy/querystring-parser" "^4.0.2" + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@smithy/util-base64@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@smithy/util-base64/-/util-base64-4.0.0.tgz#8345f1b837e5f636e5f8470c4d1706ae0c6d0358" @@ -2643,7 +3350,7 @@ "@smithy/util-buffer-from@^2.2.0": version "2.2.0" - resolved "https://registry.yarnpkg.com/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz#6fc88585165ec73f8681d426d96de5d402021e4b" + resolved "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz" integrity sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA== dependencies: "@smithy/is-array-buffer" "^2.2.0" @@ -2686,6 +3393,17 @@ bowser "^2.11.0" tslib "^2.6.2" +"@smithy/util-defaults-mode-browser@^4.0.8": + version "4.0.8" + resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.8.tgz#77bc4590cdc928901b80f3482e79607a2cbcb150" + integrity sha512-ZTypzBra+lI/LfTYZeop9UjoJhhGRTg3pxrNpfSTQLd3AJ37r2z4AXTKpq1rFXiiUIJsYyFgNJdjWRGP/cbBaQ== + dependencies: + "@smithy/property-provider" "^4.0.2" + "@smithy/smithy-client" "^4.2.0" + "@smithy/types" "^4.2.0" + bowser "^2.11.0" + tslib "^2.6.2" + "@smithy/util-defaults-mode-node@^4.0.3": version "4.0.4" resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.4.tgz#5470fdc96672cee5199620b576d7025de3b17333" @@ -2712,6 +3430,19 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@smithy/util-defaults-mode-node@^4.0.8": + version "4.0.8" + resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.8.tgz#123b517efe6434977139b341d1f64b5f1e743aac" + integrity sha512-Rgk0Jc/UDfRTzVthye/k2dDsz5Xxs9LZaKCNPgJTRyoyBoeiNCnHsYGOyu1PKN+sDyPnJzMOz22JbwxzBp9NNA== + dependencies: + "@smithy/config-resolver" "^4.1.0" + "@smithy/credential-provider-imds" "^4.0.2" + "@smithy/node-config-provider" "^4.0.2" + "@smithy/property-provider" "^4.0.2" + "@smithy/smithy-client" "^4.2.0" + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@smithy/util-endpoints@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@smithy/util-endpoints/-/util-endpoints-3.0.1.tgz#44ccbf1721447966f69496c9003b87daa8f61975" @@ -2721,6 +3452,15 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@smithy/util-endpoints@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@smithy/util-endpoints/-/util-endpoints-3.0.2.tgz#6933a0d6d4a349523ef71ca9540c9c0b222b559e" + integrity sha512-6QSutU5ZyrpNbnd51zRTL7goojlcnuOB55+F9VBD+j8JpRY50IGamsjlycrmpn8PQkmJucFW8A0LSfXj7jjtLQ== + dependencies: + "@smithy/node-config-provider" "^4.0.2" + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@smithy/util-hex-encoding@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz#dd449a6452cffb37c5b1807ec2525bb4be551e8d" @@ -2736,6 +3476,14 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@smithy/util-middleware@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@smithy/util-middleware/-/util-middleware-4.0.2.tgz#272f1249664e27068ef0d5f967a233bf7b77962c" + integrity sha512-6GDamTGLuBQVAEuQ4yDQ+ti/YINf/MEmIegrEeg7DdB/sld8BX1lqt9RRuIcABOhAGTA50bRbPzErez7SlDtDQ== + dependencies: + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@smithy/util-retry@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@smithy/util-retry/-/util-retry-4.0.1.tgz#fb5f26492383dcb9a09cc4aee23a10f839cd0769" @@ -2745,6 +3493,15 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" +"@smithy/util-retry@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@smithy/util-retry/-/util-retry-4.0.2.tgz#9b64cf460d63555884e641721d19e3c0abff8ee6" + integrity sha512-Qryc+QG+7BCpvjloFLQrmlSd0RsVRHejRXd78jNO3+oREueCjwG1CCEH1vduw/ZkM1U9TztwIKVIi3+8MJScGg== + dependencies: + "@smithy/service-error-classification" "^4.0.2" + "@smithy/types" "^4.2.0" + tslib "^2.6.2" + "@smithy/util-stream@^4.0.2": version "4.0.2" resolved "https://registry.yarnpkg.com/@smithy/util-stream/-/util-stream-4.0.2.tgz#63495d3f7fba9d78748d540921136dc4a8d4c067" @@ -2773,6 +3530,20 @@ "@smithy/util-utf8" "^4.0.0" tslib "^2.6.2" +"@smithy/util-stream@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/util-stream/-/util-stream-4.2.0.tgz#85f85516b0042726162bf619caa3358332195652" + integrity sha512-Vj1TtwWnuWqdgQI6YTUF5hQ/0jmFiOYsc51CSMgj7QfyO+RF4EnT2HNjoviNlOOmgzgvf3f5yno+EiC4vrnaWQ== + dependencies: + "@smithy/fetch-http-handler" "^5.0.2" + "@smithy/node-http-handler" "^4.0.4" + "@smithy/types" "^4.2.0" + "@smithy/util-base64" "^4.0.0" + "@smithy/util-buffer-from" "^4.0.0" + "@smithy/util-hex-encoding" "^4.0.0" + "@smithy/util-utf8" "^4.0.0" + tslib "^2.6.2" + "@smithy/util-uri-escape@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz#a96c160c76f3552458a44d8081fade519d214737" @@ -2782,7 +3553,7 @@ "@smithy/util-utf8@^2.0.0": version "2.3.0" - resolved "https://registry.yarnpkg.com/@smithy/util-utf8/-/util-utf8-2.3.0.tgz#dd96d7640363259924a214313c3cf16e7dd329c5" + resolved "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz" integrity sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A== dependencies: "@smithy/util-buffer-from" "^2.2.0" @@ -3150,7 +3921,7 @@ "@touch4it/ical-timezones@^1.9.0": version "1.9.0" - resolved "https://registry.yarnpkg.com/@touch4it/ical-timezones/-/ical-timezones-1.9.0.tgz#bbd85014f55b5cc3e9079ed7caccd8649b5170a3" + resolved "https://registry.npmjs.org/@touch4it/ical-timezones/-/ical-timezones-1.9.0.tgz" integrity sha512-UAiZMrFlgMdOIaJDPsKu5S7OecyMLr3GGALJTYkRgHmsHAA/8Ixm1qD09ELP2X7U1lqgrctEgvKj9GzMbczC+g== "@tsconfig/node22@^22.0.0": @@ -3203,7 +3974,7 @@ "@types/body-parser@*": version "1.19.5" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" + resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz" integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== dependencies: "@types/connect" "*" @@ -3211,14 +3982,14 @@ "@types/connect@*": version "3.4.38" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz" integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== dependencies: "@types/node" "*" "@types/cookiejar@^2.1.5": version "2.1.5" - resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78" + resolved "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz" integrity sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q== "@types/doctrine@^0.0.9": @@ -3228,7 +3999,7 @@ "@types/estree@1.0.6", "@types/estree@^1.0.0": version "1.0.6" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz" integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== "@types/express-serve-static-core@^4.17.33": @@ -3243,7 +4014,7 @@ "@types/express@^4.17.17": version "4.17.21" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + resolved "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz" integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== dependencies: "@types/body-parser" "*" @@ -3253,12 +4024,12 @@ "@types/http-errors@*": version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" + resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz" integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== "@types/json5@^0.0.29": version "0.0.29" - resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== "@types/jsonwebtoken@^9.0.2": @@ -3276,12 +4047,12 @@ "@types/methods@^1.1.4": version "1.1.4" - resolved "https://registry.yarnpkg.com/@types/methods/-/methods-1.1.4.tgz#d3b7ac30ac47c91054ea951ce9eed07b1051e547" + resolved "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz" integrity sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ== "@types/mime@^1": version "1.3.5" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz" integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== "@types/ms@*": @@ -3320,7 +4091,7 @@ "@types/range-parser@*": version "1.2.7" - resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz" integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== "@types/react-dom@^18.3.0": @@ -3343,7 +4114,7 @@ "@types/send@*": version "0.17.4" - resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + resolved "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz" integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== dependencies: "@types/mime" "^1" @@ -3351,7 +4122,7 @@ "@types/serve-static@*": version "1.15.7" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" + resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz" integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== dependencies: "@types/http-errors" "*" @@ -3367,7 +4138,7 @@ "@types/sinonjs__fake-timers@*": version "8.1.5" - resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz#5fd3592ff10c1e9695d377020c033116cc2889f2" + resolved "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz" integrity sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ== "@types/superagent@^8.1.0": @@ -3382,7 +4153,7 @@ "@types/supertest@^6.0.2": version "6.0.2" - resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-6.0.2.tgz#2af1c466456aaf82c7c6106c6b5cbd73a5e86588" + resolved "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz" integrity sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg== dependencies: "@types/methods" "^1.1.4" @@ -3393,6 +4164,13 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== +"@types/ws@*": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" + integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== + dependencies: + "@types/node" "*" + "@types/ws@^8.5.10": version "8.5.14" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.14.tgz#93d44b268c9127d96026cf44353725dd9b6c3c21" @@ -3626,12 +4404,12 @@ "@vladfrangu/async_event_emitter@^2.2.4", "@vladfrangu/async_event_emitter@^2.4.6": version "2.4.6" - resolved "https://registry.yarnpkg.com/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz#508b6c45b03f917112a9008180b308ba0e4d1805" + resolved "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz" integrity sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA== "@yarnpkg/lockfile@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" + resolved "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz" integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== "@zeit/schemas@2.36.0": @@ -3662,7 +4440,7 @@ abstract-cache@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/abstract-cache/-/abstract-cache-1.0.1.tgz#136151becf5c32e0ea27f78728d073d8fe07932a" + resolved "https://registry.npmjs.org/abstract-cache/-/abstract-cache-1.0.1.tgz" integrity sha512-EfUeMhRUbG5bVVbrSY/ogLlFXoyfMAPxMlSP7wrEqH53d+59r2foVy9a5KjmprLKFLOfPQCNKEfpBN/nQ76chw== dependencies: clone "^2.1.1" @@ -3671,7 +4449,7 @@ abstract-cache@^1.0.1: abstract-logging@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/abstract-logging/-/abstract-logging-2.0.1.tgz#6b0c371df212db7129b57d2e7fcf282b8bf1c839" + resolved "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz" integrity sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA== accepts@~1.3.5: @@ -3684,12 +4462,12 @@ accepts@~1.3.5: acorn-jsx@^5.2.0, acorn-jsx@^5.3.2: version "5.3.2" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn@^7.1.1: version "7.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== acorn@^8.14.0, acorn@^8.9.0: @@ -3704,7 +4482,7 @@ agent-base@^7.1.0, agent-base@^7.1.2: ajv-formats@^3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-3.0.1.tgz#3d5dc762bca17679c3c2ea7e90ad6b7532309578" + resolved "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz" integrity sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ== dependencies: ajv "^8.0.0" @@ -3721,7 +4499,7 @@ ajv@8.12.0: ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4: version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== dependencies: fast-deep-equal "^3.1.1" @@ -3731,7 +4509,7 @@ ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4: ajv@^8.0.0, ajv@^8.0.1, ajv@^8.12.0: version "8.17.1" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz" integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== dependencies: fast-deep-equal "^3.1.3" @@ -3748,19 +4526,19 @@ ansi-align@^3.0.1: ansi-escapes@^4.2.1: version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== dependencies: type-fest "^0.21.3" ansi-regex@^4.1.0: version "4.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz" integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== ansi-regex@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-regex@^6.0.1: @@ -3770,14 +4548,14 @@ ansi-regex@^6.0.1: ansi-styles@^3.2.0, ansi-styles@^3.2.1: version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== dependencies: color-convert "^1.9.0" ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" @@ -3789,7 +4567,7 @@ ansi-styles@^5.0.0: ansi-styles@^6.1.0: version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== anymatch@~3.1.2: @@ -3810,16 +4588,25 @@ arg@5.0.2: resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== +argon2@^0.41.1: + version "0.41.1" + resolved "https://registry.yarnpkg.com/argon2/-/argon2-0.41.1.tgz#30ce6b013e273bc7e92c558d40e66d35e5e8c63b" + integrity sha512-dqCW8kJXke8Ik+McUcMDltrbuAWETPyU6iq+4AhxqKphWi7pChB/Zgd/Tp/o8xRLbg8ksMj46F/vph9wnxpTzQ== + dependencies: + "@phc/format" "^1.0.0" + node-addon-api "^8.1.0" + node-gyp-build "^4.8.1" + argparse@^1.0.7: version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== dependencies: sprintf-js "~1.0.2" argparse@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== aria-query@5.1.3: @@ -3936,24 +4723,24 @@ arraybuffer.prototype.slice@^1.0.4: asap@^2.0.0: version "2.0.6" - resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz" integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== asn1@~0.2.3: version "0.2.6" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz" integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== dependencies: safer-buffer "~2.1.0" assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== assertion-error@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" + resolved "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz" integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== ast-types-flow@^0.0.8: @@ -3970,7 +4757,7 @@ ast-types@^0.16.1: astral-regex@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== astral-regex@^2.0.0: @@ -3985,29 +4772,42 @@ async-function@^1.0.0: asynckit@^0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== atomic-sleep@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" + resolved "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz" integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== 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" + resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz" integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== dependencies: possible-typed-array-names "^1.0.0" avvio@^9.0.0: version "9.1.0" - resolved "https://registry.yarnpkg.com/avvio/-/avvio-9.1.0.tgz#0ff80ed211682441d8aa39ff21a4b9d022109c44" + resolved "https://registry.npmjs.org/avvio/-/avvio-9.1.0.tgz" integrity sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw== dependencies: "@fastify/error" "^4.0.0" fastq "^1.17.1" +aws-crt@^1.24.0: + version "1.26.2" + resolved "https://registry.yarnpkg.com/aws-crt/-/aws-crt-1.26.2.tgz#b9208ed0ec5376150dca46beb0722fefc2d3b192" + integrity sha512-XyzCoWMQ693g6iLFqgeVl6DTMKZIIc0zlzwLvP47az7nRgob8JLiqJDbx1ljKqBxKesRqq9igjTMzOKh3JkvUA== + dependencies: + "@aws-sdk/util-utf8-browser" "^3.259.0" + "@httptoolkit/websocket-stream" "^6.0.1" + axios "^1.7.4" + buffer "^6.0.3" + crypto-js "^4.2.0" + mqtt "^4.3.8" + process "^0.11.10" + aws-sdk-client-mock@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/aws-sdk-client-mock/-/aws-sdk-client-mock-4.1.0.tgz#ae1950b2277f8e65f9a039975d79ff9fffab39e3" @@ -4019,7 +4819,7 @@ aws-sdk-client-mock@^4.1.0: aws-sign2@~0.7.0: version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz" integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== aws4@^1.8.0: @@ -4032,7 +4832,16 @@ axe-core@^4.10.0: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.2.tgz#85228e3e1d8b8532a27659b332e39b7fa0e022df" integrity sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w== -axios@^1.7.3, axios@^1.7.7: +axios@^1.7.4, axios@^1.8.4: + version "1.8.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.4.tgz#78990bb4bc63d2cae072952d374835950a82f447" + integrity sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +axios@^1.7.7: version "1.7.9" resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a" integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw== @@ -4048,7 +4857,7 @@ axobject-query@^4.1.0: babel-eslint@^10.0.1: version "10.1.0" - resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232" + resolved "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz" integrity sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg== dependencies: "@babel/code-frame" "^7.0.0" @@ -4060,7 +4869,7 @@ babel-eslint@^10.0.1: balanced-match@^1.0.0: version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== balanced-match@^2.0.0: @@ -4080,7 +4889,7 @@ base64-js@^1.3.1: bcrypt-pbkdf@^1.0.0: version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz" integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== dependencies: tweetnacl "^0.14.3" @@ -4097,9 +4906,18 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== +bl@^4.0.2: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + bowser@^2.11.0: version "2.11.0" - resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" + resolved "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz" integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA== boxen@7.0.0: @@ -4118,7 +4936,7 @@ boxen@7.0.0: brace-expansion@^1.1.7: version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== dependencies: balanced-match "^1.0.0" @@ -4126,14 +4944,14 @@ brace-expansion@^1.1.7: brace-expansion@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz" integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== dependencies: balanced-match "^1.0.0" braces@^3.0.3, braces@~3.0.2: version "3.0.3" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: fill-range "^7.1.1" @@ -4155,9 +4973,22 @@ browserslist@^4.24.0: buffer-equal-constant-time@1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz" integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + buffer@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" @@ -4178,7 +5009,7 @@ bytes@3.1.2: cac@^6.7.14: version "6.7.14" - resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" + resolved "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz" integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== cacheable@^1.8.8: @@ -4217,7 +5048,7 @@ call-bound@^1.0.2, call-bound@^1.0.3: callsites@^3.0.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== camelcase-css@^2.0.1: @@ -4242,7 +5073,7 @@ caniuse-lite@^1.0.30001688: caseless@~0.12.0: version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== chai@^5.1.1, chai@^5.1.2: @@ -4270,7 +5101,7 @@ chalk@5.0.1: chalk@^2.1.0: version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== dependencies: ansi-styles "^3.2.1" @@ -4287,7 +5118,7 @@ chalk@^3.0.0: chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: ansi-styles "^4.1.0" @@ -4300,15 +5131,15 @@ chalk@^5.0.1: chardet@^0.7.0: version "0.7.0" - resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== check-error@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc" + resolved "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz" integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw== -chokidar@^3.5.2: +chokidar@^3.5.2, chokidar@^3.5.3: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== @@ -4330,14 +5161,14 @@ cli-boxes@^3.0.0: cli-cursor@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz" integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== dependencies: restore-cursor "^3.1.0" cli-width@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + resolved "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== clipboardy@3.0.0: @@ -4369,7 +5200,7 @@ cliui@^8.0.1: clone@2.x, clone@^2.1.1: version "2.1.2" - resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + resolved "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz" integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== clsx@^2.0.0, clsx@^2.1.1: @@ -4379,14 +5210,14 @@ clsx@^2.0.0, clsx@^2.1.1: color-convert@^1.9.0: version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== dependencies: color-name "1.1.3" color-convert@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== dependencies: color-name "~1.1.4" @@ -4398,12 +5229,12 @@ color-loggers@^0.3.1: color-name@1.1.3: version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== color-name@~1.1.4: version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== colord@^2.9.3: @@ -4418,29 +5249,37 @@ colorette@^2.0.7: colors@1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + resolved "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== dependencies: delayed-stream "~1.0.0" commander@^2.11.0: version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== commander@^7.2.0: version "7.2.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== +commist@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/commist/-/commist-1.1.0.tgz#17811ec6978f6c15ee4de80c45c9beb77cee35d5" + integrity sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg== + dependencies: + leven "^2.1.0" + minimist "^1.1.0" + component-emitter@^1.3.0: version "1.3.1" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17" + resolved "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz" integrity sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ== compressible@~2.0.16: @@ -4465,9 +5304,19 @@ compression@1.7.4: concat-map@0.0.1: version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +concat-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" + integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.0.2" + typedarray "^0.0.6" + concurrently@^9.1.2: version "9.1.2" resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-9.1.2.tgz#22d9109296961eaee773e12bfb1ce9a66bc9836c" @@ -4510,14 +5359,19 @@ cookie@^1.0.1: cookiejar@^2.1.4: version "2.1.4" - resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" + resolved "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz" integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== 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" + resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + cosmiconfig@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-9.0.0.tgz#34c3fc58287b915f3ae905ab6dc3de258b55ad9d" @@ -4530,7 +5384,7 @@ cosmiconfig@^9.0.0: cross-env@^7.0.3: version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + resolved "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz" integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== dependencies: cross-spawn "^7.0.1" @@ -4555,6 +5409,11 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3, shebang-command "^2.0.0" which "^2.0.1" +crypto-js@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" + integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== + css-functions-list@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.2.3.tgz#95652b0c24f0f59b291a9fc386041a19d4f40dbe" @@ -4598,7 +5457,7 @@ damerau-levenshtein@^1.0.8: dashdash@^1.12.0: version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + resolved "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz" integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== dependencies: assert-plus "^1.0.0" @@ -4664,7 +5523,7 @@ debug@4, debug@^4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug debug@^3.2.7: version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" @@ -4681,7 +5540,7 @@ decimal.js@^10.4.3: deep-eql@^5.0.1: version "5.0.2" - resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341" + resolved "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz" integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q== deep-equal@^2.0.5: @@ -4715,12 +5574,12 @@ deep-extend@^0.6.0: deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.4" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + 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" + resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz" integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== dependencies: es-define-property "^1.0.0" @@ -4734,7 +5593,7 @@ define-lazy-prop@^2.0.0: define-properties@^1.1.3, define-properties@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz" integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== dependencies: define-data-property "^1.0.1" @@ -4743,7 +5602,7 @@ define-properties@^1.1.3, define-properties@^1.2.1: delayed-stream@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== depd@2.0.0: @@ -4763,7 +5622,7 @@ detect-node-es@^1.1.0: dezalgo@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" + resolved "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz" integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== dependencies: asap "^2.0.0" @@ -4816,14 +5675,14 @@ do-not-zip@^1.0.0: doctrine@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz" integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== dependencies: esutils "^2.0.2" doctrine@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== dependencies: esutils "^2.0.2" @@ -4875,6 +5734,26 @@ dunder-proto@^1.0.0, dunder-proto@^1.0.1: es-errors "^1.3.0" gopd "^1.2.0" +duplexify@^3.5.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" + integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +duplexify@^4.1.1: + version "4.1.3" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.3.tgz#a07e1c0d0a2c001158563d32592ba58bddb0236f" + integrity sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA== + dependencies: + end-of-stream "^1.4.1" + inherits "^2.0.3" + readable-stream "^3.1.1" + stream-shift "^1.0.2" + eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -4882,7 +5761,7 @@ eastasianwidth@^0.2.0: ecc-jsbn@~0.1.1: version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + resolved "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz" integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== dependencies: jsbn "~0.1.0" @@ -4890,7 +5769,7 @@ ecc-jsbn@~0.1.1: ecdsa-sig-formatter@1.0.11: version "1.0.11" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz" integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== dependencies: safe-buffer "^5.0.1" @@ -4902,12 +5781,12 @@ electron-to-chromium@^1.5.73: emoji-regex@^7.0.1: version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz" integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== emoji-regex@^8.0.0: version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== emoji-regex@^9.2.2: @@ -4915,7 +5794,7 @@ emoji-regex@^9.2.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== -end-of-stream@^1.1.0: +end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -5016,7 +5895,7 @@ es-define-property@^1.0.0, es-define-property@^1.0.1: es-errors@^1.3.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== es-get-iterator@^1.1.3: @@ -5080,7 +5959,7 @@ es-set-tostringtag@^2.0.3, es-set-tostringtag@^2.1.0: 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" + resolved "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz" integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== dependencies: hasown "^2.0.0" @@ -5099,6 +5978,16 @@ esbuild-copy-static-files@^0.1.0: resolved "https://registry.yarnpkg.com/esbuild-copy-static-files/-/esbuild-copy-static-files-0.1.0.tgz#4bb4987b5b554d2fc122a45f077d74663b4dbcf0" integrity sha512-KlpmYqANA1t2nZavEdItfcOjJC6wbHA21v35HJWN32DddGTWKNNGDKljUzbCPojmpD+wAw8/DXr5abJ4jFCE0w== +esbuild-plugin-copy@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/esbuild-plugin-copy/-/esbuild-plugin-copy-2.1.1.tgz#638308ecfd679e4c7c76b71c62f7dd9a4cc7f901" + integrity sha512-Bk66jpevTcV8KMFzZI1P7MZKZ+uDcrZm2G2egZ2jNIvVnivDpodZI+/KnpL3Jnap0PBdIHU7HwFGB8r+vV5CVw== + dependencies: + chalk "^4.1.2" + chokidar "^3.5.3" + fs-extra "^10.0.1" + globby "^11.0.3" + esbuild-register@^3.5.0: version "3.6.0" resolved "https://registry.yarnpkg.com/esbuild-register/-/esbuild-register-3.6.0.tgz#cf270cfa677baebbc0010ac024b823cbf723a36d" @@ -5139,7 +6028,7 @@ esbuild-register@^3.5.0: esbuild@^0.21.3: version "0.21.5" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz" integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== optionalDependencies: "@esbuild/aix-ppc64" "0.21.5" @@ -5208,12 +6097,12 @@ escape-html@~1.0.3: escape-string-regexp@^1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== escape-string-regexp@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== eslint-config-airbnb-base@^15.0.0: @@ -5244,7 +6133,7 @@ eslint-config-airbnb@^19.0.4: eslint-config-esnext@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/eslint-config-esnext/-/eslint-config-esnext-4.1.0.tgz#8695b858fcf40d28c1aedca181f700528c7b60c6" + resolved "https://registry.npmjs.org/eslint-config-esnext/-/eslint-config-esnext-4.1.0.tgz" integrity sha512-GhfVEXdqYKEIIj7j+Fw2SQdL9qyZMekgXfq6PyXM66cQw0B435ddjz3P3kxOBVihMRJ0xGYjosaveQz5Y6z0uA== dependencies: babel-eslint "^10.0.1" @@ -5259,12 +6148,12 @@ eslint-config-mantine@^3.2.0: eslint-config-prettier@^9.1.0: version "9.1.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f" + resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz" integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw== 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" + resolved "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz" integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== dependencies: debug "^3.2.7" @@ -5294,7 +6183,7 @@ eslint-module-utils@^2.12.0: eslint-plugin-babel@^5.2.1: version "5.3.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-babel/-/eslint-plugin-babel-5.3.1.tgz#75a2413ffbf17e7be57458301c60291f2cfbf560" + resolved "https://registry.npmjs.org/eslint-plugin-babel/-/eslint-plugin-babel-5.3.1.tgz" integrity sha512-VsQEr6NH3dj664+EyxJwO4FCYm/00JhYb3Sk3ft8o+fpKuIfQ9TaW6uVUfvwMXHcf/lsnRIoyFPsLMyiWCSL/g== dependencies: eslint-rule-composer "^0.3.0" @@ -5384,12 +6273,12 @@ eslint-plugin-react@^7.35.0: eslint-rule-composer@^0.3.0: version "0.3.0" - resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9" + resolved "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz" integrity sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg== eslint-scope@^5.0.0: version "5.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== dependencies: esrecurse "^4.3.0" @@ -5397,7 +6286,7 @@ eslint-scope@^5.0.0: eslint-scope@^7.2.2: version "7.2.2" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz" integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== dependencies: esrecurse "^4.3.0" @@ -5405,19 +6294,19 @@ eslint-scope@^7.2.2: eslint-utils@^1.4.3: version "1.4.3" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" + resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz" integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== dependencies: eslint-visitor-keys "^1.1.0" eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz" integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== 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" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== eslint-visitor-keys@^4.2.0: @@ -5427,7 +6316,7 @@ eslint-visitor-keys@^4.2.0: eslint@^6.8.0: version "6.8.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb" + resolved "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz" integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig== dependencies: "@babel/code-frame" "^7.0.0" @@ -5514,7 +6403,7 @@ eslint@^8.57.0: espree@^6.1.2: version "6.2.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a" + resolved "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz" integrity sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw== dependencies: acorn "^7.1.1" @@ -5523,7 +6412,7 @@ espree@^6.1.2: espree@^9.6.0, espree@^9.6.1: version "9.6.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + resolved "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz" integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== dependencies: acorn "^8.9.0" @@ -5532,31 +6421,31 @@ espree@^9.6.0, espree@^9.6.1: esprima@^4.0.0, esprima@~4.0.0: version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== esquery@^1.0.1, esquery@^1.4.2: version "1.6.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + resolved "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz" integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== dependencies: estraverse "^5.1.0" esrecurse@^4.3.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== dependencies: estraverse "^5.2.0" estraverse@^4.1.1: version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== estree-walker@^2.0.2: @@ -5566,14 +6455,14 @@ estree-walker@^2.0.2: estree-walker@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz" integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== dependencies: "@types/estree" "^1.0.0" esutils@^2.0.2: version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== execa@^5.1.1: @@ -5598,12 +6487,12 @@ expect-type@^1.1.0: extend@~3.0.2: version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== external-editor@^3.0.3: version "3.1.0" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + resolved "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz" integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== dependencies: chardet "^0.7.0" @@ -5612,12 +6501,12 @@ external-editor@^3.0.3: extsprintf@1.3.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz" integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== extsprintf@^1.2.0: version "1.4.1" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" + resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz" integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== fast-copy@^3.0.2: @@ -5627,17 +6516,17 @@ fast-copy@^3.0.2: fast-decode-uri-component@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz#46f8b6c22b30ff7a81357d4f59abfae938202543" + resolved "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz" integrity sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg== fast-deep-equal@3.1.3, fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-diff@^1.1.2: version "1.3.0" - resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" + resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz" integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== fast-glob@^3.2.11, fast-glob@^3.2.9, fast-glob@^3.3.2, fast-glob@^3.3.3: @@ -5653,7 +6542,7 @@ fast-glob@^3.2.11, fast-glob@^3.2.9, fast-glob@^3.3.2, fast-glob@^3.3.3: fast-json-stable-stringify@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== fast-json-stringify@^6.0.0: @@ -5670,34 +6559,34 @@ fast-json-stringify@^6.0.0: fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fast-querystring@^1.0.0: version "1.1.2" - resolved "https://registry.yarnpkg.com/fast-querystring/-/fast-querystring-1.1.2.tgz#a6d24937b4fc6f791b4ee31dcb6f53aeafb89f53" + resolved "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz" integrity sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg== dependencies: fast-decode-uri-component "^1.0.1" fast-redact@^3.1.1: version "3.5.0" - resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.5.0.tgz#e9ea02f7e57d0cd8438180083e93077e496285e4" + resolved "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz" integrity sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A== fast-safe-stringify@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + resolved "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== -fast-uri@^3.0.0, fast-uri@^3.0.1: +fast-uri@^3.0.0, fast-uri@^3.0.1, fast-uri@^3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== fast-xml-parser@4.4.1: version "4.4.1" - resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz#86dbf3f18edf8739326447bcaac31b4ae7f6514f" + resolved "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz" integrity sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw== dependencies: strnum "^1.0.5" @@ -5709,12 +6598,12 @@ fastest-levenshtein@^1.0.16: fastify-plugin@^4.5.1: version "4.5.1" - resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-4.5.1.tgz#44dc6a3cc2cce0988bc09e13f160120bbd91dbee" + resolved "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz" integrity sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ== fastify-plugin@^5.0.0: version "5.0.1" - resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-5.0.1.tgz#82d44e6fe34d1420bb5a4f7bee434d501e41939f" + resolved "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.0.1.tgz" integrity sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ== fastify-raw-body@^5.0.0: @@ -5726,10 +6615,19 @@ fastify-raw-body@^5.0.0: raw-body "^3.0.0" secure-json-parse "^2.4.0" -fastify@^5.1.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/fastify/-/fastify-5.2.1.tgz#38381800eb26b7e27da72d9ee51c544f0c52ff39" - integrity sha512-rslrNBF67eg8/Gyn7P2URV8/6pz8kSAscFL4EThZJ8JBMaXacVdVE4hmUcnPNKERl5o/xTiBSLfdowBRhVF1WA== +fastify-zod-openapi@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/fastify-zod-openapi/-/fastify-zod-openapi-4.1.1.tgz#ecf276a55eb664b52552b79af4cb1e17927cbbe8" + integrity sha512-fTx3Vb8pQXpnxF1JKUOklx1JnrihArfItc9hhmCW8ZKx5D6j3aeyl1BThl0Nh3tY7Cy6U4OgRpHxPdAlqmJ8HQ== + dependencies: + "@fastify/error" "^4.0.0" + fast-json-stringify "^6.0.0" + fastify-plugin "^5.0.0" + +fastify@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/fastify/-/fastify-5.3.2.tgz#88c895a30c0f67166979077ac8649fe8b205a1b3" + integrity sha512-AIPqBgtqBAwkOkrnwesEE+dOyU30dQ4kh7udxeGVR05CRGwubZx+p2H8P0C4cRnQT0+EPK4VGea2DTL2RtWttg== dependencies: "@fastify/ajv-compiler" "^4.0.0" "@fastify/error" "^4.0.0" @@ -5741,9 +6639,9 @@ fastify@^5.1.0: find-my-way "^9.0.0" light-my-request "^6.0.0" pino "^9.0.0" - process-warning "^4.0.0" + process-warning "^5.0.0" rfdc "^1.3.1" - secure-json-parse "^3.0.1" + secure-json-parse "^4.0.0" semver "^7.6.0" toad-cache "^3.7.0" @@ -5761,12 +6659,12 @@ fdir@^6.4.2: fflate@^0.8.2: version "0.8.2" - resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" + resolved "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz" integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== figures@^3.0.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + resolved "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz" integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== dependencies: escape-string-regexp "^1.0.5" @@ -5780,21 +6678,21 @@ file-entry-cache@^10.0.5: file-entry-cache@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" + resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz" integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== dependencies: flat-cache "^2.0.1" file-entry-cache@^6.0.1: version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== dependencies: flat-cache "^3.0.4" fill-range@^7.1.1: version "7.1.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -5818,7 +6716,7 @@ find-up@^4.1.0: find-up@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== dependencies: locate-path "^6.0.0" @@ -5826,7 +6724,7 @@ find-up@^5.0.0: flat-cache@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" + resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz" integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== dependencies: flatted "^2.0.0" @@ -5835,7 +6733,7 @@ flat-cache@^2.0.1: flat-cache@^3.0.4: version "3.2.0" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz" integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== dependencies: flatted "^3.2.9" @@ -5853,7 +6751,7 @@ flat-cache@^6.1.6: flatted@^2.0.0: version "2.0.2" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" + resolved "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz" integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== flatted@^3.2.9, flatted@^3.3.1, flatted@^3.3.2: @@ -5883,7 +6781,7 @@ foreground-child@^3.1.0: forever-agent@~0.6.1: version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== form-data@^4.0.0: @@ -5897,7 +6795,7 @@ form-data@^4.0.0: form-data@~2.3.2: version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz" integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== dependencies: asynckit "^0.4.0" @@ -5913,9 +6811,18 @@ formidable@^3.5.1: hexoid "^2.0.0" once "^1.4.0" +fs-extra@^10.0.1: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs.realpath@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== fsevents@2.3.2: @@ -5925,12 +6832,12 @@ fsevents@2.3.2: fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== function-bind@^1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== function.prototype.name@^1.1.6, function.prototype.name@^1.1.8: @@ -5947,12 +6854,12 @@ function.prototype.name@^1.1.6, function.prototype.name@^1.1.8: functional-red-black-tree@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz" integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== functions-have-names@^1.2.3: version "1.2.3" - resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== gensync@^1.0.0-beta.2: @@ -6017,21 +6924,21 @@ get-tsconfig@^4.7.5: getpass@^0.1.1: version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + resolved "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz" integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== dependencies: assert-plus "^1.0.0" glob-parent@^5.0.0, glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" glob-parent@^6.0.2: version "6.0.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== dependencies: is-glob "^4.0.3" @@ -6060,9 +6967,9 @@ glob@^11.0.0: package-json-from-dist "^1.0.0" path-scurry "^2.0.0" -glob@^7.1.3: +glob@^7.1.3, glob@^7.1.6: version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== dependencies: fs.realpath "^1.0.0" @@ -6090,19 +6997,19 @@ global-prefix@^3.0.0: globals@^11.1.0: version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^12.1.0: version "12.4.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8" + resolved "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz" integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg== dependencies: type-fest "^0.8.1" globals@^13.19.0: version "13.24.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + resolved "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz" integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== dependencies: type-fest "^0.20.2" @@ -6115,7 +7022,7 @@ globalthis@^1.0.4: define-properties "^1.2.1" gopd "^1.0.1" -globby@^11.1.0: +globby@^11.0.3, globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -6142,24 +7049,24 @@ gopd@^1.0.1, gopd@^1.2.0: resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== -graceful-fs@^4.2.4: +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== graphemer@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== har-schema@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + resolved "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz" integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q== har-validator@~5.1.3: version "5.1.5" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" + resolved "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz" integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== dependencies: ajv "^6.12.3" @@ -6177,17 +7084,17 @@ has-bigints@^1.0.2: has-flag@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== has-flag@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== has-property-descriptors@^1.0.0, 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" + resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz" integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== dependencies: es-define-property "^1.0.0" @@ -6206,18 +7113,26 @@ has-symbols@^1.0.3, has-symbols@^1.1.0: has-tostringtag@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz" integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== dependencies: has-symbols "^1.0.3" hasown@^2.0.0, hasown@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: function-bind "^1.1.2" +help-me@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/help-me/-/help-me-3.0.0.tgz#9803c81b5f346ad2bce2c6a0ba01b82257d319e8" + integrity sha512-hx73jClhyk910sidBB7ERlnhMlFsJJIBqSVMFDwPN8o2v9nmp5KgLq1Xz1Bf1fCMMZ6mPrX159iG0VLy/fPMtQ== + dependencies: + glob "^7.1.6" + readable-stream "^3.6.0" + help-me@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/help-me/-/help-me-5.0.0.tgz#b1ebe63b967b74060027c2ac61f9be12d354a6f6" @@ -6276,7 +7191,7 @@ http-proxy-agent@^7.0.2: http-signature@~1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz" integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ== dependencies: assert-plus "^1.0.0" @@ -6303,7 +7218,7 @@ husky@^9.1.4: ical-generator@^7.2.0: version "7.2.0" - resolved "https://registry.yarnpkg.com/ical-generator/-/ical-generator-7.2.0.tgz#45589146e81693065a39c6f42007abe34e07c4b9" + resolved "https://registry.npmjs.org/ical-generator/-/ical-generator-7.2.0.tgz" integrity sha512-7I34QvxWqIRthaao81lmapa0OjftfDaSBZmADjV0IqxVMUWT5ywlATRsv/hZN9Rgf2VgRsnMY+xUUaA4ZvAJLA== dependencies: uuid-random "^1.3.2" @@ -6317,7 +7232,7 @@ iconv-lite@0.6.3: iconv-lite@^0.4.24: version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" @@ -6329,7 +7244,7 @@ identity-obj-proxy@^3.0.0: dependencies: harmony-reflect "^1.4.6" -ieee754@^1.2.1: +ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -6341,7 +7256,7 @@ ignore-by-default@^1.0.1: ignore@^4.0.6: version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + resolved "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== ignore@^5.2.0, ignore@^5.3.1: @@ -6364,7 +7279,7 @@ import-fresh@^3.0.0, import-fresh@^3.2.1, import-fresh@^3.3.0: imurmurhash@^0.1.4: version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== indent-string@^4.0.0: @@ -6374,15 +7289,15 @@ indent-string@^4.0.0: inflight@^1.0.4: version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== dependencies: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== ini@^1.3.5, ini@~1.3.0: @@ -6392,7 +7307,7 @@ ini@^1.3.5, ini@~1.3.0: inquirer@^7.0.0: version "7.3.3" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" + resolved "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz" integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== dependencies: ansi-escapes "^4.2.1" @@ -6487,7 +7402,7 @@ is-bun-module@^1.0.2: is-callable@^1.2.7: version "1.2.7" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== is-core-module@^2.13.0, is-core-module@^2.15.1, is-core-module@^2.16.0: @@ -6521,7 +7436,7 @@ is-docker@^2.0.0, is-docker@^2.1.1: is-extglob@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== is-finalizationregistry@^1.1.0: @@ -6533,12 +7448,12 @@ is-finalizationregistry@^1.1.0: is-fullwidth-code-point@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz" integrity sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w== 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" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== is-generator-function@^1.0.10, is-generator-function@^1.0.7: @@ -6553,7 +7468,7 @@ is-generator-function@^1.0.10, is-generator-function@^1.0.7: 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.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: is-extglob "^2.1.1" @@ -6573,17 +7488,17 @@ is-number-object@^1.1.1: is-number@^7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== is-path-inside@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== is-plain-obj@^1.1: version "1.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz" integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg== is-plain-object@^5.0.0: @@ -6654,7 +7569,7 @@ is-typed-array@^1.1.13, is-typed-array@^1.1.14, is-typed-array@^1.1.15, is-typed is-typedarray@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== is-weakmap@^2.0.2: @@ -6686,17 +7601,27 @@ is-wsl@^2.2.0: isarray@^2.0.5: version "2.0.5" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + resolved "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz" integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + isexe@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +isomorphic-ws@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" + integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== + isstream@~0.1.2: version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0, istanbul-lib-coverage@^3.2.2: @@ -6782,7 +7707,7 @@ joi@17.4.2: jose@^4.14.6: version "4.15.9" - resolved "https://registry.yarnpkg.com/jose/-/jose-4.15.9.tgz#9b68eda29e9a0614c042fa29387196c7dd800100" + resolved "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz" integrity sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA== joycon@^3.1.1: @@ -6790,14 +7715,19 @@ joycon@^3.1.1: resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== +js-sdsl@4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.3.0.tgz#aeefe32a451f7af88425b11fdb5f58c90ae1d711" + integrity sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^3.13.1: version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== dependencies: argparse "^1.0.7" @@ -6805,14 +7735,14 @@ js-yaml@^3.13.1: js-yaml@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== dependencies: argparse "^2.0.1" jsbn@~0.1.0: version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz" integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== jsdoc-type-pratt-parser@^4.0.0: @@ -6854,7 +7784,7 @@ jsesc@^3.0.2: json-buffer@3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== json-parse-even-better-errors@^2.3.0: @@ -6869,34 +7799,43 @@ json-schema-ref-resolver@^2.0.0: dependencies: dequal "^2.0.3" +json-schema-resolver@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/json-schema-resolver/-/json-schema-resolver-3.0.0.tgz#acad9752a5817cc02f1e40371610ecf3436263ac" + integrity sha512-HqMnbz0tz2DaEJ3ntsqtx3ezzZyDE7G56A/pPY/NGmrPu76UzsWquOpHFRAf5beTNXoH2LU5cQePVvRli1nchA== + dependencies: + debug "^4.1.1" + fast-uri "^3.0.5" + rfdc "^1.1.4" + json-schema-traverse@^0.4.1: version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== json-schema-traverse@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== json-schema@0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz" integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== json-stringify-safe@~5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== json5@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + resolved "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz" integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: minimist "^1.2.0" @@ -6906,9 +7845,18 @@ json5@^2.2.2, json5@^2.2.3: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + jsonwebtoken@^9.0.0, jsonwebtoken@^9.0.2: version "9.0.2" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + resolved "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz" integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== dependencies: jws "^3.2.2" @@ -6924,7 +7872,7 @@ jsonwebtoken@^9.0.0, jsonwebtoken@^9.0.2: jsprim@^1.2.2: version "1.4.2" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" + resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz" integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== dependencies: assert-plus "1.0.0" @@ -6949,12 +7897,12 @@ jsqr@^1.4.0: just-extend@^6.2.0: version "6.2.0" - resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-6.2.0.tgz#b816abfb3d67ee860482e7401564672558163947" + resolved "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz" integrity sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw== jwa@^1.4.1: version "1.4.1" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + resolved "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz" integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== dependencies: buffer-equal-constant-time "1.0.1" @@ -6963,7 +7911,7 @@ jwa@^1.4.1: jwks-rsa@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/jwks-rsa/-/jwks-rsa-3.1.0.tgz#50406f23e38c9b2682cd437f824d7d61aa983171" + resolved "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz" integrity sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg== dependencies: "@types/express" "^4.17.17" @@ -6975,7 +7923,7 @@ jwks-rsa@^3.1.0: jws@^3.2.2: version "3.2.2" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + resolved "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz" integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== dependencies: jwa "^1.4.1" @@ -6983,7 +7931,7 @@ jws@^3.2.2: keyv@^4.5.3: version "4.5.4" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz" integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== dependencies: json-buffer "3.0.1" @@ -7022,9 +7970,14 @@ language-tags@^1.0.9: dependencies: language-subtag-registry "^0.3.20" +leven@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580" + integrity sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA== + levn@^0.3.0, levn@~0.3.0: version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + resolved "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz" integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== dependencies: prelude-ls "~1.1.2" @@ -7032,7 +7985,7 @@ levn@^0.3.0, levn@~0.3.0: levn@^0.4.1: version "0.4.1" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== dependencies: prelude-ls "^1.2.1" @@ -7049,7 +8002,7 @@ light-my-request@^6.0.0: limiter@^1.1.5: version "1.1.5" - resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2" + resolved "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz" integrity sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA== lines-and-columns@^1.1.6: @@ -7066,64 +8019,64 @@ locate-path@^5.0.0: locate-path@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== dependencies: p-locate "^5.0.0" lodash.clonedeep@^4.5.0: version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + resolved "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz" integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== lodash.get@^4.4.2: version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz" integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== lodash.includes@^4.3.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + resolved "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz" integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== lodash.isboolean@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + resolved "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz" integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== lodash.isinteger@^4.0.4: version "4.0.4" - resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + resolved "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz" integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== lodash.isnumber@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + resolved "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz" integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== lodash.isplainobject@^4.0.6: version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + resolved "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz" integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== lodash.isstring@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + resolved "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz" integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== lodash.merge@^4.6.2: version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== lodash.once@^4.0.0: version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz" integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== lodash.snakecase@4.1.1: version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d" + resolved "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz" integrity sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw== lodash.truncate@^4.4.2: @@ -7133,7 +8086,7 @@ lodash.truncate@^4.4.2: lodash@4.17.21, lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.21: version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: @@ -7148,9 +8101,9 @@ loupe@^3.1.0, loupe@^3.1.1, loupe@^3.1.2: resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.3.tgz#042a8f7986d77f3d0f98ef7990a2b2fef18b0fd2" integrity sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug== -lru-cache@6.0.0: +lru-cache@6.0.0, lru-cache@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== dependencies: yallist "^4.0.0" @@ -7174,7 +8127,7 @@ lru-cache@^5.1.1: lru-memoizer@^2.2.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/lru-memoizer/-/lru-memoizer-2.3.0.tgz#ef0fbc021bceb666794b145eefac6be49dc47f31" + resolved "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz" integrity sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug== dependencies: lodash.clonedeep "^4.5.0" @@ -7182,7 +8135,7 @@ lru-memoizer@^2.2.0: lru_map@^0.3.3: version "0.3.3" - resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" + resolved "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz" integrity sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ== lz-string@^1.5.0: @@ -7192,7 +8145,7 @@ lz-string@^1.5.0: magic-bytes.js@^1.10.0: version "1.10.0" - resolved "https://registry.yarnpkg.com/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz#c41cf4bc2f802992b05e64962411c9dd44fdef92" + resolved "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz" integrity sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ== magic-string@^0.27.0: @@ -7274,7 +8227,7 @@ meow@^13.2.0: merge-options@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-1.0.1.tgz#2a64b24457becd4e4dc608283247e94ce589aa32" + resolved "https://registry.npmjs.org/merge-options/-/merge-options-1.0.1.tgz" integrity sha512-iuPV41VWKWBIOpBsjoxjDZw8/GbSfZ2mk7N1453bwMrfzdrIk7EzBd+8UVR6rkw67th7xnk9Dytl3J+lHPdxvg== dependencies: is-plain-obj "^1.1" @@ -7286,22 +8239,22 @@ merge-refs@^1.3.0: merge-stream@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== methods@^1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== micromatch@^4.0.8: version "4.0.8" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: braces "^3.0.3" @@ -7309,7 +8262,7 @@ micromatch@^4.0.8: mime-db@1.52.0: version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== "mime-db@>= 1.43.0 < 2": @@ -7331,14 +8284,14 @@ mime-types@2.1.18: mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.34: version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: mime-db "1.52.0" mime@2.6.0: version "2.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + resolved "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz" integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== mime@^3: @@ -7348,7 +8301,7 @@ mime@^3: mimic-fn@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== min-indent@^1.0.0, min-indent@^1.0.1: @@ -7358,7 +8311,7 @@ min-indent@^1.0.0, min-indent@^1.0.1: minimatch@3.1.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" @@ -7372,14 +8325,14 @@ minimatch@^10.0.0: minimatch@^9.0.4: version "9.0.5" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz" integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== dependencies: brace-expansion "^2.0.1" -minimist@^1.2.0, minimist@^1.2.6: +minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== "minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: @@ -7389,21 +8342,21 @@ minimist@^1.2.0, minimist@^1.2.6: mkdirp@^0.5.1: version "0.5.6" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz" integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== dependencies: minimist "^1.2.6" mnemonist@0.38.3: version "0.38.3" - resolved "https://registry.yarnpkg.com/mnemonist/-/mnemonist-0.38.3.tgz#35ec79c1c1f4357cfda2fe264659c2775ccd7d9d" + resolved "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.3.tgz" integrity sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw== dependencies: obliterator "^1.6.1" mnemonist@0.39.8: version "0.39.8" - resolved "https://registry.yarnpkg.com/mnemonist/-/mnemonist-0.39.8.tgz#9078cd8386081afd986cca34b52b5d84ea7a4d38" + resolved "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz" integrity sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ== dependencies: obliterator "^2.0.1" @@ -7417,12 +8370,44 @@ moment-timezone@^0.5.45: moment@^2.29.4, moment@^2.30.1: version "2.30.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + resolved "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz" integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== +mqtt-packet@^6.8.0: + version "6.10.0" + resolved "https://registry.yarnpkg.com/mqtt-packet/-/mqtt-packet-6.10.0.tgz#c8b507832c4152e3e511c0efa104ae4a64cd418f" + integrity sha512-ja8+mFKIHdB1Tpl6vac+sktqy3gA8t9Mduom1BA75cI+R9AHnZOiaBQwpGiWnaVJLDGRdNhQmFaAqd7tkKSMGA== + dependencies: + bl "^4.0.2" + debug "^4.1.1" + process-nextick-args "^2.0.1" + +mqtt@^4.3.8: + version "4.3.8" + resolved "https://registry.yarnpkg.com/mqtt/-/mqtt-4.3.8.tgz#b8cc9a6eb5e4e0cb6eea699f24cd70dd7b228f1d" + integrity sha512-2xT75uYa0kiPEF/PE0VPdavmEkoBzMT/UL9moid0rAvlCtV48qBwxD62m7Ld/4j8tSkIO1E/iqRl/S72SEOhOw== + dependencies: + commist "^1.0.0" + concat-stream "^2.0.0" + debug "^4.1.1" + duplexify "^4.1.1" + help-me "^3.0.0" + inherits "^2.0.3" + lru-cache "^6.0.0" + minimist "^1.2.5" + mqtt-packet "^6.8.0" + number-allocator "^1.0.9" + pump "^3.0.0" + readable-stream "^3.6.0" + reinterval "^1.1.0" + rfdc "^1.3.0" + split2 "^3.1.0" + ws "^7.5.5" + xtend "^4.0.2" + mrmime@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-2.0.0.tgz#151082a6e06e59a9a39b46b3e14d5cfe92b3abb4" + resolved "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz" integrity sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw== ms@2.0.0: @@ -7432,12 +8417,12 @@ ms@2.0.0: ms@^2.1.1, ms@^2.1.3: version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== mute-stream@0.0.8: version "0.0.8" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== nanoid@^3.3.8: @@ -7447,7 +8432,7 @@ nanoid@^3.3.8: natural-compare@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== negotiator@0.6.3: @@ -7457,7 +8442,7 @@ negotiator@0.6.3: nice-try@^1.0.4: version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== nise@^6.0.0: @@ -7473,11 +8458,16 @@ nise@^6.0.0: nmtree@^1.0.6: version "1.0.6" - resolved "https://registry.yarnpkg.com/nmtree/-/nmtree-1.0.6.tgz#953e057ad545e9e627f1275bd25fea4e92c1cf63" + resolved "https://registry.npmjs.org/nmtree/-/nmtree-1.0.6.tgz" integrity sha512-SUPCoyX5w/lOT6wD/PZEymR+J899984tYEOYjuDqQlIOeX5NSb1MEsCcT0az+dhZD0MLAj5hGBZEpKQxuDdniA== dependencies: commander "^2.11.0" +node-addon-api@^8.1.0: + version "8.3.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-8.3.1.tgz#53bc8a4f8dbde3de787b9828059da94ba9fd4eed" + integrity sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA== + node-cache@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d" @@ -7490,6 +8480,11 @@ node-forge@^1.3.1: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== +node-gyp-build@^4.8.1: + version "4.8.4" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8" + integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ== + node-ical@^0.20.1: version "0.20.1" resolved "https://registry.yarnpkg.com/node-ical/-/node-ical-0.20.1.tgz#3a67319af9be956b3cc81cdf6716d1352eaefaca" @@ -7533,6 +8528,14 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +number-allocator@^1.0.9: + version "1.0.14" + resolved "https://registry.yarnpkg.com/number-allocator/-/number-allocator-1.0.14.tgz#1f2e32855498a7740dcc8c78bed54592d930ee4d" + integrity sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA== + dependencies: + debug "^4.3.1" + js-sdsl "4.3.0" + nwsapi@^2.2.12: version "2.2.16" resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.16.tgz#177760bba02c351df1d2644e220c31dfec8cdb43" @@ -7540,7 +8543,7 @@ nwsapi@^2.2.12: oauth-sign@~0.9.0: version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== object-assign@^4.1.1: @@ -7563,7 +8566,7 @@ object-is@^1.1.5: object-keys@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== object.assign@^4.1.2, object.assign@^4.1.4, object.assign@^4.1.7: @@ -7618,7 +8621,7 @@ object.values@^1.1.6, object.values@^1.2.0, object.values@^1.2.1: obliterator@^1.6.1: version "1.6.1" - resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-1.6.1.tgz#dea03e8ab821f6c4d96a299e17aef6a3af994ef3" + resolved "https://registry.npmjs.org/obliterator/-/obliterator-1.6.1.tgz" integrity sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig== obliterator@^2.0.1: @@ -7628,7 +8631,7 @@ obliterator@^2.0.1: on-exit-leak-free@^2.1.0: version "2.1.2" - resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8" + resolved "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz" integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA== on-headers@~1.0.2: @@ -7638,14 +8641,14 @@ on-headers@~1.0.2: once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== dependencies: mimic-fn "^2.1.0" @@ -7659,9 +8662,14 @@ open@^8.0.4: is-docker "^2.1.1" is-wsl "^2.2.0" +openapi-types@^12.1.3: + version "12.1.3" + resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-12.1.3.tgz#471995eb26c4b97b7bd356aacf7b91b73e777dd3" + integrity sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw== + optionator@^0.8.3: version "0.8.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz" integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== dependencies: deep-is "~0.1.3" @@ -7673,7 +8681,7 @@ optionator@^0.8.3: optionator@^0.9.3: version "0.9.4" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz" integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== dependencies: deep-is "^0.1.3" @@ -7685,7 +8693,7 @@ optionator@^0.9.3: os-tmpdir@~1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== own-keys@^1.0.1: @@ -7706,7 +8714,7 @@ p-limit@^2.2.0: p-limit@^3.0.2: version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== dependencies: yocto-queue "^0.1.0" @@ -7720,7 +8728,7 @@ p-locate@^4.1.0: p-locate@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== dependencies: p-limit "^3.0.2" @@ -7737,7 +8745,7 @@ package-json-from-dist@^1.0.0: parent-module@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== dependencies: callsites "^3.0.0" @@ -7771,12 +8779,12 @@ passkit-generator@^3.3.1: path-exists@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== path-is-absolute@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== path-is-inside@1.0.2: @@ -7786,17 +8794,17 @@ path-is-inside@1.0.2: path-key@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + resolved "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz" integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-parse@^1.0.7: version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== path-scurry@^1.11.1: @@ -7832,12 +8840,12 @@ path-type@^4.0.0: pathe@^1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" + resolved "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz" integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== pathval@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.0.tgz#7e2550b422601d4f6b8e26f1301bc8f15a741a25" + resolved "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz" integrity sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA== pdfjs-dist@4.8.69, pdfjs-dist@^4.5.136, pdfjs-dist@^4.6.82, pdfjs-dist@^4.8.69: @@ -7849,17 +8857,17 @@ pdfjs-dist@4.8.69, pdfjs-dist@^4.5.136, pdfjs-dist@^4.6.82, pdfjs-dist@^4.8.69: performance-now@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== picocolors@^1.0.0, picocolors@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== picomatch@^4.0.2: @@ -7895,7 +8903,7 @@ pino-pretty@^13.0.0: pino-std-serializers@^7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz#7c625038b13718dbbd84ab446bd673dc52259e3b" + resolved "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz" integrity sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA== pino@^9.0.0, pino@^9.6.0: @@ -7948,7 +8956,7 @@ polished@^4.2.2: 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" + resolved "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz" integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== postcss-js@^4.0.0: @@ -8040,17 +9048,17 @@ postcss@^8.4.41, postcss@^8.4.43, postcss@^8.5.1: prelude-ls@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== prelude-ls@~1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz" integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== prettier-linter-helpers@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + resolved "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz" integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== dependencies: fast-diff "^1.1.2" @@ -8069,11 +9077,21 @@ pretty-format@^27.0.2: ansi-styles "^5.0.0" react-is "^17.0.1" +process-nextick-args@^2.0.1, process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + process-warning@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-4.0.1.tgz#5c1db66007c67c756e4e09eb170cdece15da32fb" integrity sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q== +process-warning@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-5.0.0.tgz#566e0bf79d1dff30a72d8bbbe9e8ecefe8d378d7" + integrity sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA== + process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" @@ -8081,7 +9099,7 @@ process@^0.11.10: progress@^2.0.0: version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== prop-types@^15.6.2, prop-types@^15.8.1: @@ -8095,7 +9113,7 @@ prop-types@^15.6.2, prop-types@^15.8.1: proxy-from-env@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== psl@^1.1.28, psl@^1.1.33: @@ -8120,7 +9138,7 @@ pump@^3.0.0: punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1: version "2.3.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== qrcode@^1.5.4: @@ -8141,7 +9159,7 @@ qs@^6.11.0: qs@~6.5.2: version "6.5.3" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" + resolved "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz" integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== querystringify@^2.1.1: @@ -8151,17 +9169,17 @@ querystringify@^2.1.1: queue-microtask@^1.2.2: version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== quick-format-unescaped@^4.0.3: version "4.0.4" - resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" + resolved "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz" integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== random-bytes@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" + resolved "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz" integrity sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ== range-parser@1.2.0: @@ -8337,6 +9355,28 @@ react-transition-group@4.4.5: dependencies: loose-envify "^1.1.0" +readable-stream@^2.0.0, readable-stream@^2.3.3: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -8346,7 +9386,7 @@ readdirp@~3.6.0: real-require@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" + resolved "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz" integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== recast@^0.23.5: @@ -8401,7 +9441,7 @@ regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.3: regexpp@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" + resolved "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz" integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== registry-auth-token@3.3.2: @@ -8419,9 +9459,14 @@ registry-url@3.1.0: dependencies: rc "^1.0.1" +reinterval@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/reinterval/-/reinterval-1.1.0.tgz#3361ecfa3ca6c18283380dd0bb9546f390f5ece7" + integrity sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ== + request@^2.88.2: version "2.88.2" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + resolved "https://registry.npmjs.org/request/-/request-2.88.2.tgz" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== dependencies: aws-sign2 "~0.7.0" @@ -8452,7 +9497,7 @@ require-directory@^2.1.1: require-from-string@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== require-main-filename@^2.0.0: @@ -8467,7 +9512,7 @@ requires-port@^1.0.0: resolve-from@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== resolve-from@^5.0.0: @@ -8477,7 +9522,7 @@ resolve-from@^5.0.0: resolve-pkg-maps@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" + resolved "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz" integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== resolve@^1.12.0, resolve@^1.22.1, resolve@^1.22.4, resolve@^1.22.8: @@ -8500,7 +9545,7 @@ resolve@^2.0.0-next.5: restore-cursor@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz" integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== dependencies: onetime "^5.1.0" @@ -8508,29 +9553,29 @@ restore-cursor@^3.1.0: ret@~0.5.0: version "0.5.0" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.5.0.tgz#30a4d38a7e704bd96dc5ffcbe7ce2a9274c41c95" + resolved "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz" integrity sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw== reusify@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rfdc@^1.2.0, rfdc@^1.3.1: +rfdc@^1.1.4, rfdc@^1.2.0, rfdc@^1.3.0, rfdc@^1.3.1: version "1.4.1" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" + resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz" integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== rimraf@2.6.3: version "2.6.3" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz" integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== dependencies: glob "^7.1.3" rimraf@^3.0.2: version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: glob "^7.1.3" @@ -8572,7 +9617,7 @@ rollup@^4.20.0, rollup@^4.30.1: rrule@2.8.1: version "2.8.1" - resolved "https://registry.yarnpkg.com/rrule/-/rrule-2.8.1.tgz#e8341a9ce3e68ce5b8da4d502e893cd9f286805e" + resolved "https://registry.npmjs.org/rrule/-/rrule-2.8.1.tgz" integrity sha512-hM3dHSBMeaJ0Ktp7W38BJZ7O1zOgaFEsn41PDk+yHoEtfLV+PoJt9E9xAlZiWgf/iqEqionN0ebHFZIDAp+iGw== dependencies: tslib "^2.4.0" @@ -8589,19 +9634,19 @@ rrweb-cssom@^0.8.0: run-async@^2.4.0: version "2.4.1" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + resolved "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz" integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== run-parallel@^1.1.9: version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== dependencies: queue-microtask "^1.2.2" rxjs@^6.6.0: version "6.6.7" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz" integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== dependencies: tslib "^1.9.0" @@ -8624,14 +9669,14 @@ safe-array-concat@^1.1.3: has-symbols "^1.1.0" isarray "^2.0.5" -safe-buffer@5.1.2: +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.2: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== safe-push-apply@^1.0.0: @@ -8665,7 +9710,7 @@ safe-stable-stringify@^2.3.1: "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", 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" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== saxes@^6.0.0: @@ -8687,14 +9732,14 @@ secure-json-parse@^2.4.0: resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862" integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw== -secure-json-parse@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-3.0.2.tgz#255b03bb0627ba5805f64f384b0a7691d8cb021b" - integrity sha512-H6nS2o8bWfpFEV6U38sOSjS7bTbdgbCGU9wEM6W14P5H0QOsz94KCusifV44GpHDTu2nqZbuDNhTzu+mjDSw1w== +secure-json-parse@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-4.0.0.tgz#2ee1b7581be38ab348bab5a3e49280ba80a89c85" + integrity sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA== semver@^5.5.0: version "5.7.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== semver@^6.1.2, semver@^6.3.0, semver@^6.3.1: @@ -8744,12 +9789,12 @@ set-blocking@^2.0.0: set-cookie-parser@^2.6.0: version "2.7.1" - resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz#3016f150072202dfbe90fadee053573cc89d2943" + resolved "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz" integrity sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ== set-function-length@^1.2.2: version "1.2.2" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz" integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== dependencies: define-data-property "^1.1.4" @@ -8785,26 +9830,26 @@ setprototypeof@1.2.0: shebang-command@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz" integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg== dependencies: shebang-regex "^1.0.0" shebang-command@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== dependencies: shebang-regex "^3.0.0" shebang-regex@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz" integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ== shebang-regex@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== shell-quote@^1.8.1: @@ -8854,7 +9899,7 @@ side-channel@^1.0.4, side-channel@^1.1.0: siginfo@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" + resolved "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz" integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== signal-exit@^3.0.2, signal-exit@^3.0.3: @@ -8864,7 +9909,7 @@ signal-exit@^3.0.2, signal-exit@^3.0.3: signal-exit@^4.0.1: version "4.1.0" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== simple-update-notifier@^2.0.0: @@ -8902,7 +9947,7 @@ slash@^3.0.0: slice-ansi@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" + resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz" integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== dependencies: ansi-styles "^3.2.0" @@ -8927,12 +9972,12 @@ sonic-boom@^4.0.1: sort-object-keys@^1.1.3: version "1.1.3" - resolved "https://registry.yarnpkg.com/sort-object-keys/-/sort-object-keys-1.1.3.tgz#bff833fe85cab147b34742e45863453c1e190b45" + resolved "https://registry.npmjs.org/sort-object-keys/-/sort-object-keys-1.1.3.tgz" integrity sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg== source-map-js@^1.0.1, source-map-js@^1.2.0, source-map-js@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== source-map@~0.6.1: @@ -8940,19 +9985,26 @@ source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +split2@^3.1.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" + integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== + dependencies: + readable-stream "^3.0.0" + split2@^4.0.0: version "4.2.0" - resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + resolved "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz" integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== sprintf-js@~1.0.2: version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== sshpk@^1.7.0: version "1.18.0" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028" + resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz" integrity sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ== dependencies: asn1 "~0.2.3" @@ -8972,7 +10024,7 @@ stable-hash@^0.0.4: stackback@0.0.2: version "0.0.2" - resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" + resolved "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz" integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== statuses@2.0.1: @@ -9014,6 +10066,11 @@ storybook@^8.2.8: dependencies: "@storybook/core" "8.5.3" +stream-shift@^1.0.0, stream-shift@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.3.tgz#85b8fab4d71010fc3ba8772e8046cc49b8a3864b" + integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ== + "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -9025,7 +10082,7 @@ storybook@^8.2.8: string-width@^3.0.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + resolved "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz" integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== dependencies: emoji-regex "^7.0.1" @@ -9034,7 +10091,7 @@ string-width@^3.0.0: string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: emoji-regex "^8.0.0" @@ -9111,13 +10168,27 @@ string.prototype.trimend@^1.0.8, string.prototype.trimend@^1.0.9: 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" + resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz" integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== dependencies: call-bind "^1.0.7" define-properties "^1.2.1" es-object-atoms "^1.0.0" +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + "strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -9127,28 +10198,28 @@ string.prototype.trimstart@^1.0.8: strip-ansi@^5.1.0, strip-ansi@^5.2.0: version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz" integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== dependencies: ansi-regex "^4.1.0" strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" strip-ansi@^7.0.1: version "7.1.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== dependencies: ansi-regex "^6.0.1" strip-bom@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== strip-final-newline@^2.0.0: @@ -9172,7 +10243,7 @@ strip-indent@^4.0.0: strip-json-comments@^3.0.1, strip-json-comments@^3.1.1: version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== strip-json-comments@~2.0.1: @@ -9190,7 +10261,7 @@ stripe@^17.6.0: strnum@^1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" + resolved "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz" integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== stylelint-config-recommended-scss@^14.0.0: @@ -9287,7 +10358,7 @@ sugarss@^4.0.1: superagent@^9.0.1: version "9.0.2" - resolved "https://registry.yarnpkg.com/superagent/-/superagent-9.0.2.tgz#a18799473fc57557289d6b63960610e358bdebc1" + resolved "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz" integrity sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w== dependencies: component-emitter "^1.3.0" @@ -9302,7 +10373,7 @@ superagent@^9.0.1: supertest@^7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/supertest/-/supertest-7.0.0.tgz#cac53b3d6872a0b317980b2b0cfa820f09cd7634" + resolved "https://registry.npmjs.org/supertest/-/supertest-7.0.0.tgz" integrity sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA== dependencies: methods "^1.1.2" @@ -9310,7 +10381,7 @@ supertest@^7.0.0: supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== dependencies: has-flag "^3.0.0" @@ -9339,7 +10410,7 @@ supports-hyperlinks@^3.1.0: supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== svg-tags@^1.0.0: @@ -9382,7 +10453,7 @@ tabbable@^6.0.0: table@^5.2.3: version "5.4.6" - resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" + resolved "https://registry.npmjs.org/table/-/table-5.4.6.tgz" integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== dependencies: ajv "^6.10.2" @@ -9403,7 +10474,7 @@ table@^6.9.0: tapable@^2.2.0: version "2.2.1" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== test-exclude@^7.0.1: @@ -9417,19 +10488,19 @@ test-exclude@^7.0.1: text-table@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== thread-stream@^3.0.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-3.1.0.tgz#4b2ef252a7c215064507d4ef70c05a5e2d34c4f1" + resolved "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz" integrity sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A== dependencies: real-require "^0.2.0" through@^2.3.6: version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== tiny-invariant@^1.0.0, tiny-invariant@^1.3.1, tiny-invariant@^1.3.3: @@ -9462,7 +10533,7 @@ tinypool@^1.0.1: tinyrainbow@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-1.2.0.tgz#5c57d2fc0fb3d1afd78465c33ca885d04f02abb5" + resolved "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz" integrity sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ== tinyspy@^3.0.0, tinyspy@^3.0.2: @@ -9472,21 +10543,21 @@ tinyspy@^3.0.0, tinyspy@^3.0.2: tmp@^0.0.33: version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz" integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== dependencies: os-tmpdir "~1.0.2" to-regex-range@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== dependencies: is-number "^7.0.0" toad-cache@^3.7.0: version "3.7.0" - resolved "https://registry.yarnpkg.com/toad-cache/-/toad-cache-3.7.0.tgz#b9b63304ea7c45ec34d91f1d2fa513517025c441" + resolved "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz" integrity sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw== toidentifier@1.0.1: @@ -9496,7 +10567,7 @@ toidentifier@1.0.1: totalist@^3.0.0: version "3.0.1" - resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8" + resolved "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz" integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ== touch@^3.1.0: @@ -9516,7 +10587,7 @@ tough-cookie@^4.1.4: tough-cookie@~2.5.0: version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz" integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== dependencies: psl "^1.1.28" @@ -9551,7 +10622,7 @@ ts-dedent@^2.0.0, ts-dedent@^2.2.0: ts-mixer@^6.0.4: version "6.0.4" - resolved "https://registry.yarnpkg.com/ts-mixer/-/ts-mixer-6.0.4.tgz#1da39ceabc09d947a82140d9f09db0f84919ca28" + resolved "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz" integrity sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA== tsconfck@^3.0.3: @@ -9561,7 +10632,7 @@ tsconfck@^3.0.3: tsconfig-paths@^3.15.0: version "3.15.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" + resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz" integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== dependencies: "@types/json5" "^0.0.29" @@ -9580,10 +10651,10 @@ tsconfig-paths@^4.2.0: tslib@^1.9.0: version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.6.2, tslib@^2.6.3, tslib@^2.7.0: +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.6.2, tslib@^2.6.3, tslib@^2.7.0: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -9600,33 +10671,33 @@ tsx@^4.16.5: tunnel-agent@^0.6.0: version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz" integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== dependencies: safe-buffer "^5.0.1" tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== dependencies: prelude-ls "^1.2.1" type-check@~0.3.2: version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz" integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== dependencies: prelude-ls "~1.1.2" type-detect@4.0.8: version "4.0.8" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== type-detect@^4.1.0: @@ -9636,17 +10707,17 @@ type-detect@^4.1.0: type-fest@^0.20.2: version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== type-fest@^0.21.3: version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== type-fest@^0.8.1: version "0.8.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== type-fest@^2.13.0, type-fest@^2.19.0: @@ -9704,6 +10775,11 @@ typed-array-length@^1.0.7: possible-typed-array-names "^1.0.0" reflect.getprototypeof "^1.0.6" +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + typescript-eslint@^8.0.1: version "8.23.0" resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.23.0.tgz#796deb48f040146b68fcc8cb07db68b87219a8d2" @@ -9720,7 +10796,7 @@ typescript@^5.5.4: uid-safe@^2.1.5: version "2.1.5" - resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" + resolved "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz" integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA== dependencies: random-bytes "~1.0.0" @@ -9755,6 +10831,11 @@ universalify@^0.2.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + unpipe@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -9786,7 +10867,7 @@ update-check@1.5.4: uri-js@^4.2.2: version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== dependencies: punycode "^2.1.0" @@ -9831,7 +10912,7 @@ use-sidecar@^1.1.3: detect-node-es "^1.1.0" tslib "^2.0.0" -util-deprecate@^1.0.2: +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== @@ -9849,7 +10930,7 @@ util@^0.12.5: uuid-random@^1.3.2: version "1.3.2" - resolved "https://registry.yarnpkg.com/uuid-random/-/uuid-random-1.3.2.tgz#96715edbaef4e84b1dcf5024b00d16f30220e2d0" + resolved "https://registry.npmjs.org/uuid-random/-/uuid-random-1.3.2.tgz" integrity sha512-UOzej0Le/UgkbWEO8flm+0y+G+ljUon1QWTEZOq1rnMAsxo2+SckbiZdKzAHHlVh6gJqI1TjC/xwgR50MuCrBQ== uuid@^10.0.0: @@ -9864,22 +10945,22 @@ uuid@^11.0.5: uuid@^3.3.2: version "3.4.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== uuid@^8.3.0: version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== uuid@^9.0.0, uuid@^9.0.1: version "9.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== v8-compile-cache@^2.0.3: version "2.4.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz#cdada8bec61e15865f05d097c5f4fd30e94dc128" + resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz" integrity sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw== vary@~1.1.2: @@ -9889,7 +10970,7 @@ vary@~1.1.2: verror@1.10.0: version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz" integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== dependencies: assert-plus "^1.0.0" @@ -10067,21 +11148,21 @@ which-typed-array@^1.1.13, which-typed-array@^1.1.16, which-typed-array@^1.1.18, which@^1.2.9, which@^1.3.1: version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== dependencies: isexe "^2.0.0" which@^2.0.1: version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" why-is-node-running@^2.3.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04" + resolved "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz" integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== dependencies: siginfo "^2.0.0" @@ -10096,7 +11177,7 @@ widest-line@^4.0.1: word-wrap@^1.2.5, word-wrap@~1.2.3: version "1.2.5" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": @@ -10137,7 +11218,7 @@ wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: wrappy@1: version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== write-file-atomic@^5.0.1: @@ -10150,14 +11231,24 @@ write-file-atomic@^5.0.1: write@1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" + resolved "https://registry.npmjs.org/write/-/write-1.0.3.tgz" integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== dependencies: mkdirp "^0.5.1" +ws@*: + version "8.18.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb" + integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w== + +ws@^7.5.5: + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== + ws@^8.17.0, ws@^8.18.0, ws@^8.2.3: version "8.18.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + resolved "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz" integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== xml-name-validator@^5.0.0: @@ -10170,6 +11261,11 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xtend@^4.0.0, xtend@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + y18n@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" @@ -10187,9 +11283,14 @@ yallist@^3.0.2: yallist@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml@^2.4.1, yaml@^2.4.2: + version "2.7.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.1.tgz#44a247d1b88523855679ac7fa7cda6ed7e135cf6" + integrity sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ== + yargs-parser@^18.1.2: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" @@ -10242,9 +11343,14 @@ yarn-upgrade-all@^0.7.4: yocto-queue@^0.1.0: version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +zod-openapi@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/zod-openapi/-/zod-openapi-4.2.4.tgz#44a0ec81ace6d64d705c8e4232095fac313359cf" + integrity sha512-tsrQpbpqFCXqVXUzi3TPwFhuMtLN3oNZobOtYnK6/5VkXsNdnIgyNr4r8no4wmYluaxzN3F7iS+8xCW8BmMQ8g== + zod-to-json-schema@^3.23.2: version "3.24.1" resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz#f08c6725091aadabffa820ba8d50c7ab527f227a" From e18177ee672e92a5b7d69eeea6ce0ffaee2ffc9a Mon Sep 17 00:00:00 2001 From: Ethan Chang Date: Sun, 27 Apr 2025 13:16:04 -0500 Subject: [PATCH 08/13] siglead main page api working (fetching from dynamo db only) * pulls sigid and counts, displays on main page * routing to detail page working * need to implement tarash's name function * tests not written yet --- src/api/functions/siglead.ts | 40 ++++++++++++++++++- src/api/routes/siglead.ts | 42 +++++++++++++++++++- src/common/types/siglead.ts | 39 ++++++++++-------- src/ui/pages/siglead/ManageSigLeads.page.tsx | 23 ++++++++++- src/ui/pages/siglead/SigScreenComponents.tsx | 17 +++++--- 5 files changed, 135 insertions(+), 26 deletions(-) diff --git a/src/api/functions/siglead.ts b/src/api/functions/siglead.ts index 763c21af..64a1ab23 100644 --- a/src/api/functions/siglead.ts +++ b/src/api/functions/siglead.ts @@ -4,8 +4,11 @@ import { ScanCommand, } from "@aws-sdk/client-dynamodb"; import { unmarshall } from "@aws-sdk/util-dynamodb"; -import { SigDetailRecord, SigMemberRecord } from "common/types/siglead.js"; -import { FastifyRequest } from "fastify"; +import { + SigDetailRecord, + SigMemberCount, + SigMemberRecord, +} from "common/types/siglead.js"; export async function fetchMemberRecords( sigid: string, @@ -63,3 +66,36 @@ export async function fetchSigDetail( return unmarshalledItem as SigDetailRecord; })[0]; } + +// select count(sigid) +// from table +// groupby sigid +export async function fetchSigCounts( + sigMemberTableName: string, + dynamoClient: DynamoDBClient, +) { + const scan = new ScanCommand({ + TableName: sigMemberTableName, + ProjectionExpression: "sigGroupId", + }); + + const result = await dynamoClient.send(scan); + + const counts: Record = {}; + + (result.Items || []).forEach((item) => { + const sigGroupId = item.sigGroupId?.S; + if (sigGroupId) { + counts[sigGroupId] = (counts[sigGroupId] || 0) + 1; + } + }); + + const countsArray: SigMemberCount[] = Object.entries(counts).map( + ([sigid, count]) => ({ + sigid, + count, + }), + ); + console.log(countsArray); + return countsArray; +} diff --git a/src/api/routes/siglead.ts b/src/api/routes/siglead.ts index 6e9e6cec..33c3e785 100644 --- a/src/api/routes/siglead.ts +++ b/src/api/routes/siglead.ts @@ -40,9 +40,14 @@ import { zodToJsonSchema } from "zod-to-json-schema"; import { SigDetailRecord, SigleadGetRequest, + SigMemberCount, SigMemberRecord, } from "common/types/siglead.js"; -import { fetchMemberRecords, fetchSigDetail } from "api/functions/siglead.js"; +import { + fetchMemberRecords, + fetchSigCounts, + fetchSigDetail, +} from "api/functions/siglead.js"; import { intersection } from "api/plugins/auth.js"; const sigleadRoutes: FastifyPluginAsync = async (fastify, _options) => { @@ -124,7 +129,42 @@ const sigleadRoutes: FastifyPluginAsync = async (fastify, _options) => { reply.code(200).send(sigDetail); }, ); + + // fetch sig count + fastify.get( + "/sigcount", + { + onRequest: async (request, reply) => { + /*await fastify.authorize(request, reply, [ + AppRoles.LINKS_MANAGER, + AppRoles.LINKS_ADMIN, + ]);*/ + }, + }, + async (request, reply) => { + // First try-catch: Fetch owner records + let sigMemCounts: SigMemberCount[]; + try { + sigMemCounts = await fetchSigCounts( + genericConfig.SigleadDynamoSigMemberTableName, + fastify.dynamoClient, + ); + } catch (error) { + request.log.error( + `Failed to fetch sig member counts record: ${error instanceof Error ? error.toString() : "Unknown error"}`, + ); + throw new DatabaseFetchError({ + message: + "Failed to fetch sig member counts record from Dynamo table.", + }); + } + + // Send the response + reply.code(200).send(sigMemCounts); + }, + ); }; + fastify.register(limitedRoutes); }; diff --git a/src/common/types/siglead.ts b/src/common/types/siglead.ts index 48e3a2d9..a697ec89 100644 --- a/src/common/types/siglead.ts +++ b/src/common/types/siglead.ts @@ -1,18 +1,23 @@ export type SigDetailRecord = { - sigid: string; - signame: string; - description: string; - }; - - export type SigMemberRecord = { - sigGroupId: string; - email: string; - designation: string; - memberName: string; - }; - - export type SigleadGetRequest = { - Params: { sigid: string }; - Querystring: undefined; - Body: undefined; - }; \ No newline at end of file + sigid: string; + signame: string; + description: string; +}; + +export type SigMemberRecord = { + sigGroupId: string; + email: string; + designation: string; + memberName: string; +}; + +export type SigleadGetRequest = { + Params: { sigid: string }; + Querystring: undefined; + Body: undefined; +}; + +export type SigMemberCount = { + sigid: string; + count: number; +}; \ No newline at end of file diff --git a/src/ui/pages/siglead/ManageSigLeads.page.tsx b/src/ui/pages/siglead/ManageSigLeads.page.tsx index 4420f50b..32994748 100644 --- a/src/ui/pages/siglead/ManageSigLeads.page.tsx +++ b/src/ui/pages/siglead/ManageSigLeads.page.tsx @@ -25,6 +25,7 @@ import { ScreenComponent } from './SigScreenComponents'; import { GroupMemberGetResponse } from '@common/types/iam'; import { transformCommaSeperatedName } from '@common/utils'; import { orgsGroupId } from '@common/config'; +import { SigMemberCount } from '@common/types/siglead'; export function capitalizeFirstLetter(string: string) { return string.charAt(0).toUpperCase() + string.slice(1); @@ -65,6 +66,7 @@ type EventPostRequest = z.infer; export const ManageSigLeadsPage: React.FC = () => { const [isSubmitting, setIsSubmitting] = useState(false); + const [SigMemberCounts, setSigMemberCounts] = useState([]); const navigate = useNavigate(); const api = useApi('core'); @@ -105,6 +107,25 @@ export const ManageSigLeadsPage: React.FC = () => { getEvent(); }, [eventId, isEditing]); + useEffect(() => { + const getMemberCounts = async () => { + try { + console.log('fetching counts'); + /*const formValues = { + }; + form.setValues(formValues);*/ + const sigMemberCountsRequest = await api.get(`/api/v1/siglead/sigcount`); + setSigMemberCounts(sigMemberCountsRequest.data); + } catch (error) { + console.error('Error fetching sig member counts:', error); + notifications.show({ + message: 'Failed to fetch sig member counts, please try again.', + }); + } + }; + getMemberCounts(); + }, []); // empty dependency array to only run once + const form = useForm({ validate: zodResolver(requestBodySchema), initialValues: { @@ -204,7 +225,7 @@ export const ManageSigLeadsPage: React.FC = () => { SigLead Management System - + {/* */} diff --git a/src/ui/pages/siglead/SigScreenComponents.tsx b/src/ui/pages/siglead/SigScreenComponents.tsx index f34cf995..77104f00 100644 --- a/src/ui/pages/siglead/SigScreenComponents.tsx +++ b/src/ui/pages/siglead/SigScreenComponents.tsx @@ -3,13 +3,16 @@ import { OrganizationList } from '@common/orgs'; import { NavLink, Paper } from '@mantine/core'; import { IconUsersGroup } from '@tabler/icons-react'; import { useLocation } from 'react-router-dom'; +import { SigMemberCount } from '@common/types/siglead'; -const renderSigLink = (org: string, index: number) => { +const renderSigLink = (sigMemCount: SigMemberCount, index: number) => { const color = 'light-dark(var(--mantine-color-black), var(--mantine-color-white))'; const size = '18px'; + const org = sigMemCount.sigid; + const count = sigMemCount.count; return ( { fontSize: `${size}`, }} > - MemberCount[{index}] + {count} } @@ -38,7 +41,10 @@ const renderSigLink = (org: string, index: number) => { ); }; -export const ScreenComponent: React.FC = () => { +type props = { + SigMemberCounts: SigMemberCount[]; +}; +export const ScreenComponent: React.FC = ({ SigMemberCounts }) => { return ( <> { Organization Member Count - {OrganizationList.map(renderSigLink)} + {/* {OrganizationList.map(renderSigLink)} */} + {SigMemberCounts.map(renderSigLink)} ); }; From 70626921358e54427a3429259622f3f908e9107c Mon Sep 17 00:00:00 2001 From: Ethan Chang Date: Sun, 27 Apr 2025 13:24:28 -0500 Subject: [PATCH 09/13] remove test button --- src/ui/pages/siglead/ManageSigLeads.page.tsx | 27 ++++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/ui/pages/siglead/ManageSigLeads.page.tsx b/src/ui/pages/siglead/ManageSigLeads.page.tsx index 32994748..b7d5a1ee 100644 --- a/src/ui/pages/siglead/ManageSigLeads.page.tsx +++ b/src/ui/pages/siglead/ManageSigLeads.page.tsx @@ -206,24 +206,23 @@ export const ManageSigLeadsPage: React.FC = () => { } }; - const TestButton: React.FC = () => { - return ( - - ); - }; + // const TestButton: React.FC = () => { + // return ( + // + // ); + // }; return ( - SigLead Management System {/* */} From c256f430a9c7593b4afbed9e7b12aa66489ae041 Mon Sep 17 00:00:00 2001 From: Ethan Chang Date: Sun, 27 Apr 2025 15:10:56 -0500 Subject: [PATCH 10/13] fixed routing path --- src/ui/pages/siglead/SigScreenComponents.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/pages/siglead/SigScreenComponents.tsx b/src/ui/pages/siglead/SigScreenComponents.tsx index 77104f00..1dbc9f66 100644 --- a/src/ui/pages/siglead/SigScreenComponents.tsx +++ b/src/ui/pages/siglead/SigScreenComponents.tsx @@ -12,7 +12,7 @@ const renderSigLink = (sigMemCount: SigMemberCount, index: number) => { const count = sigMemCount.count; return ( Date: Tue, 29 Apr 2025 15:06:15 -0500 Subject: [PATCH 11/13] signame -> sigid function done with tests --- package.json | 2 +- src/common/utils.ts | 45 +++ tests/unit/common/utils.test.ts | 145 +++++++++- yarn.lock | 469 +++++++++++++++++++++++++++++++- 4 files changed, 654 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 3f065e98..68d4cddc 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "typescript": "^5.5.4", "typescript-eslint": "^8.0.1", "vite-tsconfig-paths": "^5.0.1", - "vitest": "^2.0.5", + "vitest": "^3.1.2", "yarn-upgrade-all": "^0.7.4" }, "resolutions": { diff --git a/src/common/utils.ts b/src/common/utils.ts index 786c998f..94372580 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -12,3 +12,48 @@ export function transformCommaSeperatedName(name: string) { } return name; } + +const notUnreservedCharsRegex = /[^a-zA-Z0-9\-._~]/g; +const reservedCharsRegex = /[:\/?#\[\]@!$&'()*+,;=]/g; +/** + * Transforms an organization name (sig lead) into a URI-friendly format. + * The function performs the following transformations: + * - Removes characters that are reserved or not unreserved. + * - Adds spaces between camel case words. + * - Converts reserved characters to spaces. + * - Converts all characters to lowercase and replaces all types of whitespace with hyphens. + * - Replaces any sequence of repeated hyphens with a single hyphen. + * - Refer to RFC 3986 https://datatracker.ietf.org/doc/html/rfc3986#section-2.3 + * + * @param {string} org - The organization (sig lead) name to be transformed. + * @returns {string} - The transformed organization name, ready for use as a URL. + */ +export function transformSigLeadToURI(org: string) { + console.log(`org\t${org}`) + org = org + // change not reserved chars to spaces + .trim() + .replace(notUnreservedCharsRegex, " ") + .trim() + .replace(/\s/g, "-") + + // remove all that is reserved or not unreserved + .replace(reservedCharsRegex, "") + + // convert SIG -> sig for camel case + .replace(/SIG/g, "sig") + + // add hyphen for camel case + .replace(/([a-z])([A-Z])/g, "$1-$2") + + // lower + .toLowerCase() + + // add spaces between chars and numbers (seq2seq -> seq-2-seq) + .replace(/(?<=[a-z])([0-9]+)(?=[a-z])/g, "-$1-") + + // remove duplicate hyphens + .replace(/-{2,}/g, "-"); + + return org === "-" ? "" : org; +} \ No newline at end of file diff --git a/tests/unit/common/utils.test.ts b/tests/unit/common/utils.test.ts index 15177175..e22d642c 100644 --- a/tests/unit/common/utils.test.ts +++ b/tests/unit/common/utils.test.ts @@ -1,5 +1,5 @@ import { expect, test, describe } from "vitest"; -import { transformCommaSeperatedName } from "../../../src/common/utils.js"; +import { transformCommaSeperatedName, transformSigLeadToURI } from "../../../src/common/utils.js"; describe("Comma-seperated name transformer tests", () => { test("Already-transformed names are returned as-is", () => { @@ -27,3 +27,146 @@ describe("Comma-seperated name transformer tests", () => { expect(output).toEqual(", Test"); }); }); + +describe("transformSigLeadToURI tests", () => { + + // Basic Functionality Tests + test("should convert simple names with spaces to lowercase hyphenated", () => { + const output = transformSigLeadToURI("SIG Network"); + expect(output).toEqual("sig-network"); + }); + + test("should convert simple names to lowercase", () => { + const output = transformSigLeadToURI("Testing"); + expect(output).toEqual("testing"); + }); + + test("should handle names already in the desired format", () => { + const output = transformSigLeadToURI("already-transformed-name"); + expect(output).toEqual("already-transformed-name"); + }); + + // Camel Case Tests + test("should add hyphens between camelCase words", () => { + const output = transformSigLeadToURI("SIGAuth"); + expect(output).toEqual("sig-auth"); + }); + + test("should handle multiple camelCase words", () => { + const output = transformSigLeadToURI("SuperCamelCaseProject"); + expect(output).toEqual("super-camel-case-project"); + }); + + test("should handle mixed camelCase and spaces", () => { + const output = transformSigLeadToURI("SIG ContribEx"); // SIG Contributor Experience + expect(output).toEqual("sig-contrib-ex"); + }); + + test("should handle camelCase starting with lowercase", () => { + const output = transformSigLeadToURI("myCamelCaseName"); + expect(output).toEqual("my-camel-case-name"); + }); + + // Reserved Character Tests (RFC 3986 gen-delims and sub-delims) + test("should convert reserved characters like & to hyphens", () => { + const output = transformSigLeadToURI("SIG Storage & Backup"); + expect(output).toEqual("sig-storage-backup"); // & -> space -> hyphen + }); + + test("should convert reserved characters like / and : to hyphens", () => { + const output = transformSigLeadToURI("Project:Alpha/Beta"); + expect(output).toEqual("project-alpha-beta"); // : -> space, / -> space, space+space -> hyphen + }); + + test("should convert reserved characters like () and + to hyphens", () => { + const output = transformSigLeadToURI("My Project (Test+Alpha)"); + expect(output).toEqual("my-project-test-alpha"); + }); + + test("should convert various reserved characters #[]@?$, to hyphens", () => { + const output = transformSigLeadToURI("Special#Chars[Test]?@Value,$"); + expect(output).toEqual("special-chars-test-value"); + }); + + // Non-Allowed Character Removal Tests + test("should remove characters not unreserved or reserved (e.g., ™, ©)", () => { + const output = transformSigLeadToURI("MyOrg™ With © Symbols"); + expect(output).toEqual("my-org-with-symbols"); + }); + + test("should remove emoji", () => { + const output = transformSigLeadToURI("Project ✨ Fun"); + expect(output).toEqual("project-fun"); + }); + + + // Whitespace and Hyphen Collapsing Tests + test("should handle multiple spaces between words", () => { + const output = transformSigLeadToURI("SIG UI Project"); + expect(output).toEqual("sig-ui-project"); + }); + + test("should handle leading/trailing whitespace", () => { + const output = transformSigLeadToURI(" Leading and Trailing "); + expect(output).toEqual("leading-and-trailing"); + }); + + test("should handle mixed whitespace (tabs, newlines)", () => { + const output = transformSigLeadToURI("Mix\tOf\nWhite Space"); + expect(output).toEqual("mix-of-white-space"); + }); + + test("should collapse multiple hyphens resulting from transformations", () => { + const output = transformSigLeadToURI("Test--Multiple / Spaces"); + expect(output).toEqual("test-multiple-spaces"); + }); + + test("should collapse hyphens from start/end after transformations", () => { + const output = transformSigLeadToURI("&Another Test!"); + expect(output).toEqual("another-test"); + }); + + // Unreserved Character Tests (RFC 3986) + test("should keep unreserved characters: hyphen, period, underscore, tilde", () => { + const output = transformSigLeadToURI("Keep.These-Chars_Okay~123"); + expect(output).toEqual("keep.these-chars_okay~123"); + }); + + test("should handle unreserved chars next to reserved chars", () => { + const output = transformSigLeadToURI("Test._~&Stuff"); + expect(output).toEqual("test._~-stuff"); + }); + + + // Edge Case Tests + test("should return an empty string for an empty input", () => { + const output = transformSigLeadToURI(""); + expect(output).toEqual(""); + }); + + test("should return an empty string for input with only spaces", () => { + const output = transformSigLeadToURI(" "); + expect(output).toEqual(""); + }); + + test("should return an empty string for input with only reserved/non-allowed chars and spaces", () => { + const output = transformSigLeadToURI(" & / # ™ © "); + expect(output).toEqual(""); + }); + + test("should handle numbers correctly", () => { + const output = transformSigLeadToURI("ProjectApollo11"); + expect(output).toEqual("project-apollo11"); // Number doesn't trigger camel case break after letter + }); + + test("should handle numbers triggering camel case break", () => { + const output = transformSigLeadToURI("Project11Apollo"); + expect(output).toEqual("project-11-apollo"); // Letter after number triggers camel case break + }); + + test("should handle names starting with lowercase", () => { + const output = transformSigLeadToURI("myOrg"); + expect(output).toEqual("my-org"); + }); + +}); diff --git a/yarn.lock b/yarn.lock index 6e1f3ed0..139196a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1722,6 +1722,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz#38848d3e25afe842a7943643cbcd387cc6e13461" integrity sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA== +"@esbuild/aix-ppc64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz#014180d9a149cffd95aaeead37179433f5ea5437" + integrity sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ== + "@esbuild/android-arm64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" @@ -1737,6 +1742,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz#f592957ae8b5643129fa889c79e69cd8669bb894" integrity sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg== +"@esbuild/android-arm64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz#649e47e04ddb24a27dc05c395724bc5f4c55cbfe" + integrity sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ== + "@esbuild/android-arm@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" @@ -1752,6 +1762,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.24.2.tgz#72d8a2063aa630308af486a7e5cbcd1e134335b3" integrity sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q== +"@esbuild/android-arm@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.3.tgz#8a0f719c8dc28a4a6567ef7328c36ea85f568ff4" + integrity sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A== + "@esbuild/android-x64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" @@ -1767,6 +1782,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.24.2.tgz#9a7713504d5f04792f33be9c197a882b2d88febb" integrity sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw== +"@esbuild/android-x64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.3.tgz#e2ab182d1fd06da9bef0784a13c28a7602d78009" + integrity sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ== + "@esbuild/darwin-arm64@0.21.5": version "0.21.5" resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz" @@ -1782,6 +1802,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz#02ae04ad8ebffd6e2ea096181b3366816b2b5936" integrity sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA== +"@esbuild/darwin-arm64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz#c7f3166fcece4d158a73dcfe71b2672ca0b1668b" + integrity sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w== + "@esbuild/darwin-x64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" @@ -1797,6 +1822,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz#9ec312bc29c60e1b6cecadc82bd504d8adaa19e9" integrity sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA== +"@esbuild/darwin-x64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz#d8c5342ec1a4bf4b1915643dfe031ba4b173a87a" + integrity sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A== + "@esbuild/freebsd-arm64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" @@ -1812,6 +1842,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz#5e82f44cb4906d6aebf24497d6a068cfc152fa00" integrity sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg== +"@esbuild/freebsd-arm64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz#9f7d789e2eb7747d4868817417cc968ffa84f35b" + integrity sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw== + "@esbuild/freebsd-x64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" @@ -1827,6 +1862,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz#3fb1ce92f276168b75074b4e51aa0d8141ecce7f" integrity sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q== +"@esbuild/freebsd-x64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz#8ad35c51d084184a8e9e76bb4356e95350a64709" + integrity sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q== + "@esbuild/linux-arm64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" @@ -1842,6 +1882,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz#856b632d79eb80aec0864381efd29de8fd0b1f43" integrity sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg== +"@esbuild/linux-arm64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz#3af0da3d9186092a9edd4e28fa342f57d9e3cd30" + integrity sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A== + "@esbuild/linux-arm@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" @@ -1857,6 +1902,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz#c846b4694dc5a75d1444f52257ccc5659021b736" integrity sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA== +"@esbuild/linux-arm@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz#e91cafa95e4474b3ae3d54da12e006b782e57225" + integrity sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ== + "@esbuild/linux-ia32@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" @@ -1872,6 +1922,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz#f8a16615a78826ccbb6566fab9a9606cfd4a37d5" integrity sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw== +"@esbuild/linux-ia32@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz#81025732d85b68ee510161b94acdf7e3007ea177" + integrity sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw== + "@esbuild/linux-loong64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" @@ -1887,6 +1942,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz#1c451538c765bf14913512c76ed8a351e18b09fc" integrity sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ== +"@esbuild/linux-loong64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz#3c744e4c8d5e1148cbe60a71a11b58ed8ee5deb8" + integrity sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g== + "@esbuild/linux-mips64el@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" @@ -1902,6 +1962,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz#0846edeefbc3d8d50645c51869cc64401d9239cb" integrity sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw== +"@esbuild/linux-mips64el@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz#1dfe2a5d63702db9034cc6b10b3087cc0424ec26" + integrity sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag== + "@esbuild/linux-ppc64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" @@ -1917,6 +1982,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz#8e3fc54505671d193337a36dfd4c1a23b8a41412" integrity sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw== +"@esbuild/linux-ppc64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz#2e85d9764c04a1ebb346dc0813ea05952c9a5c56" + integrity sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg== + "@esbuild/linux-riscv64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" @@ -1932,6 +2002,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz#6a1e92096d5e68f7bb10a0d64bb5b6d1daf9a694" integrity sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q== +"@esbuild/linux-riscv64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz#a9ea3334556b09f85ccbfead58c803d305092415" + integrity sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA== + "@esbuild/linux-s390x@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" @@ -1947,6 +2022,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz#ab18e56e66f7a3c49cb97d337cd0a6fea28a8577" integrity sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw== +"@esbuild/linux-s390x@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz#f6a7cb67969222b200974de58f105dfe8e99448d" + integrity sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ== + "@esbuild/linux-x64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" @@ -1962,11 +2042,21 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz#8140c9b40da634d380b0b29c837a0b4267aff38f" integrity sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q== +"@esbuild/linux-x64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz#a237d3578ecdd184a3066b1f425e314ade0f8033" + integrity sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA== + "@esbuild/netbsd-arm64@0.24.2": version "0.24.2" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz#65f19161432bafb3981f5f20a7ff45abb2e708e6" integrity sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw== +"@esbuild/netbsd-arm64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz#4c15c68d8149614ddb6a56f9c85ae62ccca08259" + integrity sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA== + "@esbuild/netbsd-x64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" @@ -1982,6 +2072,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz#7a3a97d77abfd11765a72f1c6f9b18f5396bcc40" integrity sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw== +"@esbuild/netbsd-x64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz#12f6856f8c54c2d7d0a8a64a9711c01a743878d5" + integrity sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g== + "@esbuild/openbsd-arm64@0.23.1": version "0.23.1" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz#05c5a1faf67b9881834758c69f3e51b7dee015d7" @@ -1992,6 +2087,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz#58b00238dd8f123bfff68d3acc53a6ee369af89f" integrity sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A== +"@esbuild/openbsd-arm64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz#ca078dad4a34df192c60233b058db2ca3d94bc5c" + integrity sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ== + "@esbuild/openbsd-x64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" @@ -2007,6 +2107,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz#0ac843fda0feb85a93e288842936c21a00a8a205" integrity sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA== +"@esbuild/openbsd-x64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz#c9178adb60e140e03a881d0791248489c79f95b2" + integrity sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w== + "@esbuild/sunos-x64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" @@ -2022,6 +2127,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz#8b7aa895e07828d36c422a4404cc2ecf27fb15c6" integrity sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig== +"@esbuild/sunos-x64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz#03765eb6d4214ff27e5230af779e80790d1ee09f" + integrity sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA== + "@esbuild/win32-arm64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" @@ -2037,6 +2147,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz#c023afb647cabf0c3ed13f0eddfc4f1d61c66a85" integrity sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ== +"@esbuild/win32-arm64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz#f1c867bd1730a9b8dfc461785ec6462e349411ea" + integrity sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ== + "@esbuild/win32-ia32@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" @@ -2052,6 +2167,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz#96c356132d2dda990098c8b8b951209c3cd743c2" integrity sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA== +"@esbuild/win32-ia32@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz#77491f59ef6c9ddf41df70670d5678beb3acc322" + integrity sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew== + "@esbuild/win32-x64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" @@ -2067,6 +2187,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz#34aa0b52d0fbb1a654b596acfa595f0c7b77a77b" integrity sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg== +"@esbuild/win32-x64@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz#b17a2171f9074df9e91bfb07ef99a892ac06412a" + integrity sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg== + "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.1" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz#d1145bf2c20132d6400495d6df4bf59362fd9d56" @@ -2587,96 +2712,196 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.3.tgz#eb1b0a1d75c5f048b8d41eb30188c22292676c02" integrity sha512-8kq/NjMKkMTGKMPldWihncOl62kgnLYk7cW+/4NCUWfS70/wz4+gQ7rMxMMpZ3dIOP/xw7wKNzIuUnN/H2GfUg== +"@rollup/rollup-android-arm-eabi@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz#e1562d360bca73c7bef6feef86098de3a2f1d442" + integrity sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw== + "@rollup/rollup-android-arm64@4.34.3": version "4.34.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.3.tgz#850f0962a7a98a698dfc4b7530a3932b486d84c0" integrity sha512-1PqMHiuRochQ6++SDI7SaRDWJKr/NgAlezBi5nOne6Da6IWJo3hK0TdECBDwd92IUDPG4j/bZmWuwOnomNT8wA== +"@rollup/rollup-android-arm64@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz#37ba63940211673e15dcc5f469a78e34276dbca7" + integrity sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw== + "@rollup/rollup-darwin-arm64@4.34.3": version "4.34.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.3.tgz#150c4cfacd11ca3fe2904a25bcfd3f948aa8fd39" integrity sha512-fqbrykX4mGV3DlCDXhF4OaMGcchd2tmLYxVt3On5oOZWVDFfdEoYAV2alzNChl8OzNaeMAGqm1f7gk7eIw/uDg== +"@rollup/rollup-darwin-arm64@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz#58b1eb86d997d71dabc5b78903233a3c27438ca0" + integrity sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA== + "@rollup/rollup-darwin-x64@4.34.3": version "4.34.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.3.tgz#27501960a733043c2b0634c884d20cd456d1cdef" integrity sha512-8Wxrx/KRvMsTyLTbdrMXcVKfpW51cCNW8x7iQD72xSEbjvhCY3b+w83Bea3nQfysTMR7K28esc+ZFITThXm+1w== +"@rollup/rollup-darwin-x64@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz#5e22dab3232b1e575d930ce891abb18fe19c58c9" + integrity sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw== + "@rollup/rollup-freebsd-arm64@4.34.3": version "4.34.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.3.tgz#a54caebb98ab71aaf67e826cc9e6a145fb30ffb5" integrity sha512-lpBmV2qSiELh+ATQPTjQczt5hvbTLsE0c43Rx4bGxN2VpnAZWy77we7OO62LyOSZNY7CzjMoceRPc+Lt4e9J6A== +"@rollup/rollup-freebsd-arm64@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz#04c892d9ff864d66e31419634726ab0bebb33707" + integrity sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw== + "@rollup/rollup-freebsd-x64@4.34.3": version "4.34.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.3.tgz#2312f47788b3e334b14edb7eee748e9d545fd856" integrity sha512-sNPvBIXpgaYcI6mAeH13GZMXFrrw5mdZVI1M9YQPRG2LpjwL8DSxSIflZoh/B5NEuOi53kxsR/S2GKozK1vDXA== +"@rollup/rollup-freebsd-x64@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz#f4b1e091f7cf5afc9e3a029d70128ad56409ecfb" + integrity sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q== + "@rollup/rollup-linux-arm-gnueabihf@4.34.3": version "4.34.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.3.tgz#aaaa3f678ab3bcdf8ebda600ed2a9f04fe00d9cc" integrity sha512-MW6N3AoC61OfE1VgnN5O1OW0gt8VTbhx9s/ZEPLBM11wEdHjeilPzOxVmmsrx5YmejpGPvez8QwGGvMU+pGxpw== +"@rollup/rollup-linux-arm-gnueabihf@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz#c8814bb5ce047a81b1fe4a33628dfd4ac52bd864" + integrity sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg== + "@rollup/rollup-linux-arm-musleabihf@4.34.3": version "4.34.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.3.tgz#ec7c8d98c79091afda6804fdf72d1d202217b9e4" integrity sha512-2SQkhr5xvatYq0/+H6qyW0zvrQz9LM4lxGkpWURLoQX5+yP8MsERh4uWmxFohOvwCP6l/+wgiHZ1qVwLDc7Qmw== +"@rollup/rollup-linux-arm-musleabihf@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz#5b4e7bd83cbebbf5ffe958802dcfd4ee34bf73a3" + integrity sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg== + "@rollup/rollup-linux-arm64-gnu@4.34.3": version "4.34.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.3.tgz#df198a61a48db932426eee593f3699aa289e90f5" integrity sha512-R3JLYt8YoRwKI5shJsovLpcR6pwIMui/MGG/MmxZ1DYI3iRSKI4qcYrvYgDf4Ss2oCR3RL3F3dYK7uAGQgMIuQ== +"@rollup/rollup-linux-arm64-gnu@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz#141c848e53cee011e82a11777b8a51f1b3e8d77c" + integrity sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg== + "@rollup/rollup-linux-arm64-musl@4.34.3": version "4.34.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.3.tgz#97b231d2ca6fdeaa8d0e02de2f1f3896bedf14a3" integrity sha512-4XQhG8v/t3S7Rxs7rmFUuM6j09hVrTArzONS3fUZ6oBRSN/ps9IPQjVhp62P0W3KhqJdQADo/MRlYRMdgxr/3w== +"@rollup/rollup-linux-arm64-musl@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz#22ebeaf2fa301aa4aa6c84b760e6cd1d1ac7eb1e" + integrity sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ== + "@rollup/rollup-linux-loongarch64-gnu@4.34.3": version "4.34.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.3.tgz#a1149b186e16d009d8fd715285e84ed63ba3cbbc" integrity sha512-QlW1jCUZ1LHUIYCAK2FciVw1ptHsxzApYVi05q7bz2A8oNE8QxQ85NhM4arLxkAlcnS42t4avJbSfzSQwbIaKg== +"@rollup/rollup-linux-loongarch64-gnu@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz#20b77dc78e622f5814ff8e90c14c938ceb8043bc" + integrity sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ== + "@rollup/rollup-linux-powerpc64le-gnu@4.34.3": version "4.34.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.3.tgz#df3c2c25f800bc0bdf5e8cfc00372b5ac761bc5b" integrity sha512-kMbLToizVeCcN69+nnm20Dh0hrRIAjgaaL+Wh0gWZcNt8e542d2FUGtsyuNsHVNNF3gqTJrpzUGIdwMGLEUM7g== +"@rollup/rollup-linux-powerpc64le-gnu@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz#2c90f99c987ef1198d4f8d15d754c286e1f07b13" + integrity sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg== + "@rollup/rollup-linux-riscv64-gnu@4.34.3": version "4.34.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.3.tgz#c57b3e2c12969586f3513295cb36da96746edbf6" integrity sha512-YgD0DnZ3CHtvXRH8rzjVSxwI0kMTr0RQt3o1N92RwxGdx7YejzbBO0ELlSU48DP96u1gYYVWfUhDRyaGNqJqJg== +"@rollup/rollup-linux-riscv64-gnu@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz#9336fd5e47d7f4760d02aa85f76976176eef53ca" + integrity sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ== + +"@rollup/rollup-linux-riscv64-musl@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz#d75b4d54d46439bb5c6c13762788f57e798f5670" + integrity sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA== + "@rollup/rollup-linux-s390x-gnu@4.34.3": version "4.34.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.3.tgz#e6ac0788471a9f7400b358eb5f91292efcc900c4" integrity sha512-dIOoOz8altjp6UjAi3U9EW99s8nta4gzi52FeI45GlPyrUH4QixUoBMH9VsVjt+9A2RiZBWyjYNHlJ/HmJOBCQ== +"@rollup/rollup-linux-s390x-gnu@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz#e9f09b802f1291839247399028beaef9ce034c81" + integrity sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg== + "@rollup/rollup-linux-x64-gnu@4.34.3": version "4.34.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.3.tgz#017bb2808665d69ba55740cae02708ea8cb45885" integrity sha512-lOyG3aF4FTKrhpzXfMmBXgeKUUXdAWmP2zSNf8HTAXPqZay6QYT26l64hVizBjq+hJx3pl0DTEyvPi9sTA6VGA== +"@rollup/rollup-linux-x64-gnu@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz#0413169dc00470667dea8575c1129d4e7a73eb29" + integrity sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ== + "@rollup/rollup-linux-x64-musl@4.34.3": version "4.34.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.3.tgz#ac3de953f8e31b08f1528e17f0524af15b2df38c" integrity sha512-usztyYLu2i+mYzzOjqHZTaRXbUOqw3P6laNUh1zcqxbPH1P2Tz/QdJJCQSnGxCtsRQeuU2bCyraGMtMumC46rw== +"@rollup/rollup-linux-x64-musl@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz#c76fd593323c60ea219439a00da6c6d33ffd0ea6" + integrity sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ== + "@rollup/rollup-win32-arm64-msvc@4.34.3": version "4.34.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.3.tgz#183fb4b849accdf68d430894ada2b88eea95a140" integrity sha512-ojFOKaz/ZyalIrizdBq2vyc2f0kFbJahEznfZlxdB6pF9Do6++i1zS5Gy6QLf8D7/S57MHrmBLur6AeRYeQXSA== +"@rollup/rollup-win32-arm64-msvc@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz#c7724c386eed0bda5ae7143e4081c1910cab349b" + integrity sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg== + "@rollup/rollup-win32-ia32-msvc@4.34.3": version "4.34.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.3.tgz#3fd1b93867442ecd3d2329b902b111853600cc6c" integrity sha512-K/V97GMbNa+Da9mGcZqmSl+DlJmWfHXTuI9V8oB2evGsQUtszCl67+OxWjBKpeOnYwox9Jpmt/J6VhpeRCYqow== +"@rollup/rollup-win32-ia32-msvc@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz#7749e1b65cb64fe6d41ad1ad9e970a0ccc8ac350" + integrity sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA== + "@rollup/rollup-win32-x64-msvc@4.34.3": version "4.34.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.3.tgz#2cd47d213ddd921bab1470a3e31312ee37aac08a" integrity sha512-CUypcYP31Q8O04myV6NKGzk9GVXslO5EJNfmARNSzLF2A+5rmZUlDJ4et6eoJaZgBT9wrC2p4JZH04Vkic8HdQ== +"@rollup/rollup-win32-x64-msvc@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz#8078b71fe0d5825dcbf83d52a7dc858b39da165c" + integrity sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA== + "@rtsao/scc@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" @@ -4002,6 +4227,11 @@ resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz" integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== +"@types/estree@1.0.7": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" + integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== + "@types/express-serve-static-core@^4.17.33": version "4.19.6" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz#e01324c2a024ff367d92c66f48553ced0ab50267" @@ -4316,6 +4546,16 @@ chai "^5.1.2" tinyrainbow "^1.2.0" +"@vitest/expect@3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-3.1.2.tgz#b203a7ad2efa6af96c85f6c116216bda259d2bc8" + integrity sha512-O8hJgr+zREopCAqWl3uCVaOdqJwZ9qaDwUP7vy3Xigad0phZe9APxKhPcDNqYYi0rX5oMvwJMSCAXY2afqeTSA== + dependencies: + "@vitest/spy" "3.1.2" + "@vitest/utils" "3.1.2" + chai "^5.2.0" + tinyrainbow "^2.0.0" + "@vitest/mocker@2.1.9": version "2.1.9" resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.9.tgz#36243b27351ca8f4d0bbc4ef91594ffd2dc25ef5" @@ -4325,6 +4565,15 @@ estree-walker "^3.0.3" magic-string "^0.30.12" +"@vitest/mocker@3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-3.1.2.tgz#1ff239036072feb543ab56825ada09b12a075af2" + integrity sha512-kOtd6K2lc7SQ0mBqYv/wdGedlqPdM/B38paPY+OwJ1XiNi44w3Fpog82UfOibmHaV9Wod18A09I9SCKLyDMqgw== + dependencies: + "@vitest/spy" "3.1.2" + estree-walker "^3.0.3" + magic-string "^0.30.17" + "@vitest/pretty-format@2.0.5": version "2.0.5" resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.0.5.tgz#91d2e6d3a7235c742e1a6cc50e7786e2f2979b1e" @@ -4339,6 +4588,13 @@ dependencies: tinyrainbow "^1.2.0" +"@vitest/pretty-format@3.1.2", "@vitest/pretty-format@^3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-3.1.2.tgz#689b0604c0b73fdccb144f11b64d70c9233b23b8" + integrity sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w== + dependencies: + tinyrainbow "^2.0.0" + "@vitest/runner@2.1.9": version "2.1.9" resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.9.tgz#cc18148d2d797fd1fd5908d1f1851d01459be2f6" @@ -4347,6 +4603,14 @@ "@vitest/utils" "2.1.9" pathe "^1.1.2" +"@vitest/runner@3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-3.1.2.tgz#ffeba74618046221e944e94f09b565af772170cf" + integrity sha512-bhLib9l4xb4sUMPXnThbnhX2Yi8OutBMA8Yahxa7yavQsFDtwY/jrUZwpKp2XH9DhRFJIeytlyGpXCqZ65nR+g== + dependencies: + "@vitest/utils" "3.1.2" + pathe "^2.0.3" + "@vitest/snapshot@2.1.9": version "2.1.9" resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.9.tgz#24260b93f798afb102e2dcbd7e61c6dfa118df91" @@ -4356,6 +4620,15 @@ magic-string "^0.30.12" pathe "^1.1.2" +"@vitest/snapshot@3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-3.1.2.tgz#46c52a417afbf1fe94fba0a5735cbedf9cfc60f6" + integrity sha512-Q1qkpazSF/p4ApZg1vfZSQ5Yw6OCQxVMVrLjslbLFA1hMDrT2uxtqMaw8Tc/jy5DLka1sNs1Y7rBcftMiaSH/Q== + dependencies: + "@vitest/pretty-format" "3.1.2" + magic-string "^0.30.17" + pathe "^2.0.3" + "@vitest/spy@2.0.5": version "2.0.5" resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.0.5.tgz#590fc07df84a78b8e9dd976ec2090920084a2b9f" @@ -4370,6 +4643,13 @@ dependencies: tinyspy "^3.0.2" +"@vitest/spy@3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-3.1.2.tgz#3a5be04d71c4a458c8d6859503626e2aed61bcbf" + integrity sha512-OEc5fSXMws6sHVe4kOFyDSj/+4MSwst0ib4un0DlcYgQvRuYQ0+M2HyqGaauUMnjq87tmUaMNDxKQx7wNfVqPA== + dependencies: + tinyspy "^3.0.2" + "@vitest/ui@^2.0.5": version "2.1.9" resolved "https://registry.yarnpkg.com/@vitest/ui/-/ui-2.1.9.tgz#9e876cf3caf492dd6fddbd7f87b2d6bf7186a7a9" @@ -4402,6 +4682,15 @@ loupe "^3.1.2" tinyrainbow "^1.2.0" +"@vitest/utils@3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-3.1.2.tgz#f3ae55b3a205c88c346a2a8dcde7c89210364932" + integrity sha512-5GGd0ytZ7BH3H6JTj9Kw7Prn1Nbg0wZVrIvou+UWxm54d+WoXXgAgjFJ8wn3LdagWLFSEfpPeyYrByZaGEZHLg== + dependencies: + "@vitest/pretty-format" "3.1.2" + loupe "^3.1.3" + tinyrainbow "^2.0.0" + "@vladfrangu/async_event_emitter@^2.2.4", "@vladfrangu/async_event_emitter@^2.4.6": version "2.4.6" resolved "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz" @@ -5087,6 +5376,17 @@ chai@^5.1.1, chai@^5.1.2: loupe "^3.1.0" pathval "^2.0.0" +chai@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/chai/-/chai-5.2.0.tgz#1358ee106763624114addf84ab02697e411c9c05" + integrity sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw== + dependencies: + assertion-error "^2.0.1" + check-error "^2.1.1" + deep-eql "^5.0.1" + loupe "^3.1.0" + pathval "^2.0.0" + chalk-template@0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/chalk-template/-/chalk-template-0.4.0.tgz#692c034d0ed62436b9062c1707fadcd0f753204b" @@ -5514,7 +5814,7 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.7: +debug@4, debug@^4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.7, debug@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== @@ -5940,6 +6240,11 @@ es-module-lexer@^1.5.4: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.6.0.tgz#da49f587fd9e68ee2404fe4e256c0c7d3a81be21" integrity sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ== +es-module-lexer@^1.6.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz#9159601561880a85f2734560a9099b2c31e5372a" + integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== + es-object-atoms@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" @@ -6085,6 +6390,37 @@ esbuild@^0.23.0, esbuild@~0.23.0: "@esbuild/win32-ia32" "0.23.1" "@esbuild/win32-x64" "0.23.1" +esbuild@^0.25.0: + version "0.25.3" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.3.tgz#371f7cb41283e5b2191a96047a7a89562965a285" + integrity sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q== + optionalDependencies: + "@esbuild/aix-ppc64" "0.25.3" + "@esbuild/android-arm" "0.25.3" + "@esbuild/android-arm64" "0.25.3" + "@esbuild/android-x64" "0.25.3" + "@esbuild/darwin-arm64" "0.25.3" + "@esbuild/darwin-x64" "0.25.3" + "@esbuild/freebsd-arm64" "0.25.3" + "@esbuild/freebsd-x64" "0.25.3" + "@esbuild/linux-arm" "0.25.3" + "@esbuild/linux-arm64" "0.25.3" + "@esbuild/linux-ia32" "0.25.3" + "@esbuild/linux-loong64" "0.25.3" + "@esbuild/linux-mips64el" "0.25.3" + "@esbuild/linux-ppc64" "0.25.3" + "@esbuild/linux-riscv64" "0.25.3" + "@esbuild/linux-s390x" "0.25.3" + "@esbuild/linux-x64" "0.25.3" + "@esbuild/netbsd-arm64" "0.25.3" + "@esbuild/netbsd-x64" "0.25.3" + "@esbuild/openbsd-arm64" "0.25.3" + "@esbuild/openbsd-x64" "0.25.3" + "@esbuild/sunos-x64" "0.25.3" + "@esbuild/win32-arm64" "0.25.3" + "@esbuild/win32-ia32" "0.25.3" + "@esbuild/win32-x64" "0.25.3" + escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" @@ -6485,6 +6821,11 @@ expect-type@^1.1.0: resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.1.0.tgz#a146e414250d13dfc49eafcfd1344a4060fa4c75" integrity sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA== +expect-type@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.2.1.tgz#af76d8b357cf5fa76c41c09dafb79c549e75f71f" + integrity sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw== + extend@~3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" @@ -6657,6 +6998,11 @@ fdir@^6.4.2: resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.3.tgz#011cdacf837eca9b811c89dbb902df714273db72" integrity sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw== +fdir@^6.4.4: + version "6.4.4" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.4.tgz#1cfcf86f875a883e19a8fab53622cfe992e8d2f9" + integrity sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg== + fflate@^0.8.2: version "0.8.2" resolved "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz" @@ -8096,7 +8442,7 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" -loupe@^3.1.0, loupe@^3.1.1, loupe@^3.1.2: +loupe@^3.1.0, loupe@^3.1.1, loupe@^3.1.2, loupe@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.3.tgz#042a8f7986d77f3d0f98ef7990a2b2fef18b0fd2" integrity sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug== @@ -8155,7 +8501,7 @@ magic-string@^0.27.0: dependencies: "@jridgewell/sourcemap-codec" "^1.4.13" -magic-string@^0.30.0, magic-string@^0.30.12: +magic-string@^0.30.0, magic-string@^0.30.12, magic-string@^0.30.17: version "0.30.17" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453" integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA== @@ -8843,6 +9189,11 @@ pathe@^1.1.2: resolved "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz" integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== +pathe@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== + pathval@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz" @@ -9046,6 +9397,15 @@ postcss@^8.4.41, postcss@^8.4.43, postcss@^8.5.1: picocolors "^1.1.1" source-map-js "^1.2.1" +postcss@^8.5.3: + version "8.5.3" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.3.tgz#1463b6f1c7fb16fe258736cba29a2de35237eafb" + integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A== + dependencies: + nanoid "^3.3.8" + picocolors "^1.1.1" + source-map-js "^1.2.1" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" @@ -9615,6 +9975,35 @@ rollup@^4.20.0, rollup@^4.30.1: "@rollup/rollup-win32-x64-msvc" "4.34.3" fsevents "~2.3.2" +rollup@^4.34.9: + version "4.40.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.40.1.tgz#03d6c53ebb6a9c2c060ae686a61e72a2472b366f" + integrity sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw== + dependencies: + "@types/estree" "1.0.7" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.40.1" + "@rollup/rollup-android-arm64" "4.40.1" + "@rollup/rollup-darwin-arm64" "4.40.1" + "@rollup/rollup-darwin-x64" "4.40.1" + "@rollup/rollup-freebsd-arm64" "4.40.1" + "@rollup/rollup-freebsd-x64" "4.40.1" + "@rollup/rollup-linux-arm-gnueabihf" "4.40.1" + "@rollup/rollup-linux-arm-musleabihf" "4.40.1" + "@rollup/rollup-linux-arm64-gnu" "4.40.1" + "@rollup/rollup-linux-arm64-musl" "4.40.1" + "@rollup/rollup-linux-loongarch64-gnu" "4.40.1" + "@rollup/rollup-linux-powerpc64le-gnu" "4.40.1" + "@rollup/rollup-linux-riscv64-gnu" "4.40.1" + "@rollup/rollup-linux-riscv64-musl" "4.40.1" + "@rollup/rollup-linux-s390x-gnu" "4.40.1" + "@rollup/rollup-linux-x64-gnu" "4.40.1" + "@rollup/rollup-linux-x64-musl" "4.40.1" + "@rollup/rollup-win32-arm64-msvc" "4.40.1" + "@rollup/rollup-win32-ia32-msvc" "4.40.1" + "@rollup/rollup-win32-x64-msvc" "4.40.1" + fsevents "~2.3.2" + rrule@2.8.1: version "2.8.1" resolved "https://registry.npmjs.org/rrule/-/rrule-2.8.1.tgz" @@ -10037,6 +10426,11 @@ std-env@^3.8.0: resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.8.0.tgz#b56ffc1baf1a29dcc80a3bdf11d7fca7c315e7d5" integrity sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w== +std-env@^3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.9.0.tgz#1a6f7243b339dca4c9fd55e1c7504c77ef23e8f1" + integrity sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw== + stop-iteration-iterator@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz#f481ff70a548f6124d0312c3aa14cbfa7aa542ad" @@ -10513,7 +10907,7 @@ tinybench@^2.9.0: resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== -tinyexec@^0.3.1: +tinyexec@^0.3.1, tinyexec@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2" integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== @@ -10526,7 +10920,15 @@ tinyglobby@^0.2.10: fdir "^6.4.2" picomatch "^4.0.2" -tinypool@^1.0.1: +tinyglobby@^0.2.13: + version "0.2.13" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.13.tgz#a0e46515ce6cbcd65331537e57484af5a7b2ff7e" + integrity sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw== + dependencies: + fdir "^6.4.4" + picomatch "^4.0.2" + +tinypool@^1.0.1, tinypool@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.2.tgz#706193cc532f4c100f66aa00b01c42173d9051b2" integrity sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA== @@ -10536,6 +10938,11 @@ tinyrainbow@^1.2.0: resolved "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz" integrity sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ== +tinyrainbow@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-2.0.0.tgz#9509b2162436315e80e3eee0fcce4474d2444294" + integrity sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw== + tinyspy@^3.0.0, tinyspy@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a" @@ -10988,6 +11395,17 @@ vite-node@2.1.9: pathe "^1.1.2" vite "^5.0.0" +vite-node@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-3.1.2.tgz#b17869a12307f5260b20ba4b58cf493afee70aa7" + integrity sha512-/8iMryv46J3aK13iUXsei5G/A3CUlW4665THCPS+K8xAaqrVWiGB4RfXMQXCLjpK9P2eK//BczrVkn5JLAk6DA== + dependencies: + cac "^6.7.14" + debug "^4.4.0" + es-module-lexer "^1.6.0" + pathe "^2.0.3" + vite "^5.0.0 || ^6.0.0" + vite-tsconfig-paths@^5.0.1: version "5.1.4" resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz#d9a71106a7ff2c1c840c6f1708042f76a9212ed4" @@ -11008,6 +11426,20 @@ vite@^5.0.0: optionalDependencies: fsevents "~2.3.3" +"vite@^5.0.0 || ^6.0.0": + version "6.3.3" + resolved "https://registry.yarnpkg.com/vite/-/vite-6.3.3.tgz#497392c3f2243194e4dbf09ea83e9a3dddf49b88" + integrity sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw== + dependencies: + esbuild "^0.25.0" + fdir "^6.4.4" + picomatch "^4.0.2" + postcss "^8.5.3" + rollup "^4.34.9" + tinyglobby "^0.2.13" + optionalDependencies: + fsevents "~2.3.3" + vite@^6.0.11: version "6.1.0" resolved "https://registry.yarnpkg.com/vite/-/vite-6.1.0.tgz#00a4e99a23751af98a2e4701c65ba89ce23858a6" @@ -11045,6 +11477,33 @@ vitest@^2.0.5: vite-node "2.1.9" why-is-node-running "^2.3.0" +vitest@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-3.1.2.tgz#63afc16b6da3bea6e39f5387d80719e70634ba66" + integrity sha512-WaxpJe092ID1C0mr+LH9MmNrhfzi8I65EX/NRU/Ld016KqQNRgxSOlGNP1hHN+a/F8L15Mh8klwaF77zR3GeDQ== + dependencies: + "@vitest/expect" "3.1.2" + "@vitest/mocker" "3.1.2" + "@vitest/pretty-format" "^3.1.2" + "@vitest/runner" "3.1.2" + "@vitest/snapshot" "3.1.2" + "@vitest/spy" "3.1.2" + "@vitest/utils" "3.1.2" + chai "^5.2.0" + debug "^4.4.0" + expect-type "^1.2.1" + magic-string "^0.30.17" + pathe "^2.0.3" + std-env "^3.9.0" + tinybench "^2.9.0" + tinyexec "^0.3.2" + tinyglobby "^0.2.13" + tinypool "^1.0.2" + tinyrainbow "^2.0.0" + vite "^5.0.0 || ^6.0.0" + vite-node "3.1.2" + why-is-node-running "^2.3.0" + w3c-xmlserializer@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c" From db23ccc393cf511dc99f05048bc05ab42268b1ef Mon Sep 17 00:00:00 2001 From: Ethan Chang Date: Tue, 29 Apr 2025 15:24:07 -0500 Subject: [PATCH 12/13] integrated signame->sigid to mainscreen --- src/api/functions/siglead.ts | 20 +++++++++++++++++--- src/common/orgs.ts | 2 +- src/common/types/siglead.ts | 1 + src/ui/pages/siglead/SigScreenComponents.tsx | 7 ++++--- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/api/functions/siglead.ts b/src/api/functions/siglead.ts index 64a1ab23..413ca4c1 100644 --- a/src/api/functions/siglead.ts +++ b/src/api/functions/siglead.ts @@ -4,11 +4,14 @@ import { ScanCommand, } from "@aws-sdk/client-dynamodb"; import { unmarshall } from "@aws-sdk/util-dynamodb"; +import { OrganizationList } from "common/orgs.js"; import { SigDetailRecord, SigMemberCount, SigMemberRecord, } from "common/types/siglead.js"; +import { transformSigLeadToURI } from "common/utils.js"; +import { string } from "zod"; export async function fetchMemberRecords( sigid: string, @@ -81,8 +84,13 @@ export async function fetchSigCounts( const result = await dynamoClient.send(scan); - const counts: Record = {}; + const ids2Name: Record = {}; + OrganizationList.forEach((org) => { + const sigid = transformSigLeadToURI(org); + ids2Name[sigid] = org; + }); + const counts: Record = {}; (result.Items || []).forEach((item) => { const sigGroupId = item.sigGroupId?.S; if (sigGroupId) { @@ -90,9 +98,15 @@ export async function fetchSigCounts( } }); - const countsArray: SigMemberCount[] = Object.entries(counts).map( - ([sigid, count]) => ({ + const joined: Record = {}; + Object.keys(counts).forEach((sigid) => { + joined[sigid] = [ids2Name[sigid], counts[sigid]]; + }); + + const countsArray: SigMemberCount[] = Object.entries(joined).map( + ([sigid, [signame, count]]) => ({ sigid, + signame, count, }), ); diff --git a/src/common/orgs.ts b/src/common/orgs.ts index 61d570d2..ee84d00e 100644 --- a/src/common/orgs.ts +++ b/src/common/orgs.ts @@ -4,7 +4,7 @@ export const SIGList = [ "GameBuilders", "SIGAIDA", "SIGGRAPH", - "ICPC", + "SIGICPC", "SIGMobile", "SIGMusic", "GLUG", diff --git a/src/common/types/siglead.ts b/src/common/types/siglead.ts index a697ec89..da642313 100644 --- a/src/common/types/siglead.ts +++ b/src/common/types/siglead.ts @@ -19,5 +19,6 @@ export type SigleadGetRequest = { export type SigMemberCount = { sigid: string; + signame: string; count: number; }; \ No newline at end of file diff --git a/src/ui/pages/siglead/SigScreenComponents.tsx b/src/ui/pages/siglead/SigScreenComponents.tsx index 1dbc9f66..3be3af69 100644 --- a/src/ui/pages/siglead/SigScreenComponents.tsx +++ b/src/ui/pages/siglead/SigScreenComponents.tsx @@ -8,13 +8,14 @@ import { SigMemberCount } from '@common/types/siglead'; const renderSigLink = (sigMemCount: SigMemberCount, index: number) => { const color = 'light-dark(var(--mantine-color-black), var(--mantine-color-white))'; const size = '18px'; - const org = sigMemCount.sigid; + const name = sigMemCount.signame; + const id = sigMemCount.sigid; const count = sigMemCount.count; return ( Date: Wed, 7 May 2025 23:01:52 -0500 Subject: [PATCH 13/13] Add member to sig WIP --- src/api/functions/siglead.ts | 86 ++++++++++++- src/api/routes/siglead.ts | 128 +++++++++++++++++++- src/common/types/siglead.ts | 7 ++ src/ui/pages/siglead/ViewSigLead.page.tsx | 139 ++++++++++++++-------- 4 files changed, 309 insertions(+), 51 deletions(-) diff --git a/src/api/functions/siglead.ts b/src/api/functions/siglead.ts index 413ca4c1..6061a76d 100644 --- a/src/api/functions/siglead.ts +++ b/src/api/functions/siglead.ts @@ -1,17 +1,28 @@ import { + DeleteItemCommand, DynamoDBClient, + PutItemCommand, QueryCommand, ScanCommand, } from "@aws-sdk/client-dynamodb"; -import { unmarshall } from "@aws-sdk/util-dynamodb"; +import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; +import { + EntraFetchError, + EntraGroupError, + EntraPatchError, + NotImplementedError, +} from "common/errors/index.js"; import { OrganizationList } from "common/orgs.js"; import { SigDetailRecord, + SigEntraRecord, SigMemberCount, SigMemberRecord, } from "common/types/siglead.js"; import { transformSigLeadToURI } from "common/utils.js"; import { string } from "zod"; +import { getEntraIdToken, modifyGroup } from "./entraId.js"; +import { EntraGroupActions } from "common/types/iam.js"; export async function fetchMemberRecords( sigid: string, @@ -62,7 +73,6 @@ export async function fetchSigDetail( return (result.Items || [{}]).map((item) => { const unmarshalledItem = unmarshall(item); - // Strip '#' from access field delete unmarshalledItem.leadGroupId; delete unmarshalledItem.memberGroupId; @@ -70,6 +80,35 @@ export async function fetchSigDetail( })[0]; } +export async function fetchSigEntraDetail( + sigid: string, + tableName: string, + dynamoClient: DynamoDBClient, +) { + const fetchSigDetail = new QueryCommand({ + TableName: tableName, + KeyConditionExpression: "#sigid = :accessVal", + ExpressionAttributeNames: { + "#sigid": "sigid", + }, + ExpressionAttributeValues: { + ":accessVal": { S: sigid }, + }, + ScanIndexForward: false, + }); + + const result = await dynamoClient.send(fetchSigDetail); + + // Process the results + return (result.Items || [{}]).map((item) => { + const unmarshalledItem = unmarshall(item); + + delete unmarshalledItem.description; + + return unmarshalledItem as SigEntraRecord; + })[0]; +} + // select count(sigid) // from table // groupby sigid @@ -113,3 +152,46 @@ export async function fetchSigCounts( console.log(countsArray); return countsArray; } + +export async function addMemberRecordToSig( + newMemberRecord: SigMemberRecord, + sigMemberTableName: string, + dynamoClient: DynamoDBClient, + entraIdToken: string, +) { + await dynamoClient.send( + new PutItemCommand({ + TableName: sigMemberTableName, + Item: marshall(newMemberRecord), + }), + ); + try { + const sigEntraDetails: SigEntraRecord = await fetchSigEntraDetail( + newMemberRecord.sigGroupId, + sigMemberTableName, + dynamoClient, + ); + await modifyGroup( + entraIdToken, + newMemberRecord.email, + sigEntraDetails.memberGroupId, + EntraGroupActions.ADD, + dynamoClient, + ); + } catch (e: unknown) { + // restore original Dynamo status if AAD update fails. + await dynamoClient.send( + new DeleteItemCommand({ + TableName: sigMemberTableName, + Key: { + sigGroupId: { S: newMemberRecord.sigGroupId }, + email: { S: newMemberRecord.email }, + }, + }), + ); + throw new EntraPatchError({ + message: "Could not add member to sig AAD group.", + email: newMemberRecord.email, + }); + } +} diff --git a/src/api/routes/siglead.ts b/src/api/routes/siglead.ts index 33c3e785..c485a8e3 100644 --- a/src/api/routes/siglead.ts +++ b/src/api/routes/siglead.ts @@ -1,4 +1,4 @@ -import { FastifyPluginAsync } from "fastify"; +import fastify, { FastifyPluginAsync } from "fastify"; import { z } from "zod"; import { AppRoles } from "../../common/roles.js"; import { @@ -22,12 +22,14 @@ import { TransactWriteItem, GetItemCommand, TransactionCanceledException, + InternalServerError, } from "@aws-sdk/client-dynamodb"; import { CloudFrontKeyValueStoreClient } from "@aws-sdk/client-cloudfront-keyvaluestore"; import { genericConfig, EVENT_CACHED_DURATION, LinkryGroupUUIDToGroupNameMap, + roleArns, } from "../../common/config.js"; import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; import rateLimiter from "api/plugins/rateLimiter.js"; @@ -44,13 +46,62 @@ import { SigMemberRecord, } from "common/types/siglead.js"; import { + addMemberRecordToSig, fetchMemberRecords, fetchSigCounts, fetchSigDetail, } from "api/functions/siglead.js"; import { intersection } from "api/plugins/auth.js"; +import { + FastifyZodOpenApiSchema, + FastifyZodOpenApiTypeProvider, +} from "fastify-zod-openapi"; +import { withRoles, withTags } from "api/components/index.js"; +import { AnyARecord } from "dns"; +import { getEntraIdToken } from "api/functions/entraId.js"; +import { getRoleCredentials } from "api/functions/sts.js"; +import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; +import { logger } from "api/sqs/logger.js"; +import fastifyStatic from "@fastify/static"; + +const postAddSigMemberSchema = z.object({ + sigGroupId: z.string().min(1), + email: z.string().min(1), // TODO: verify email and @illinois.edu + designation: z.string().min(1).max(1), + memberName: z.string().min(1), +}); const sigleadRoutes: FastifyPluginAsync = async (fastify, _options) => { + const getAuthorizedClients = async () => { + if (roleArns.Entra) { + fastify.log.info( + `Attempting to assume Entra role ${roleArns.Entra} to get the Entra token...`, + ); + const credentials = await getRoleCredentials(roleArns.Entra); + const clients = { + smClient: new SecretsManagerClient({ + region: genericConfig.AwsRegion, + credentials, + }), + dynamoClient: new DynamoDBClient({ + region: genericConfig.AwsRegion, + credentials, + }), + }; + fastify.log.info( + `Assumed Entra role ${roleArns.Entra} to get the Entra token.`, + ); + return clients; + } else { + fastify.log.debug( + "Did not assume Entra role as no env variable was present", + ); + return { + smClient: fastify.secretsManagerClient, + dynamoClient: fastify.dynamoClient, + }; + } + }; const limitedRoutes: FastifyPluginAsync = async (fastify) => { /*fastify.register(rateLimiter, { limit: 30, @@ -163,6 +214,81 @@ const sigleadRoutes: FastifyPluginAsync = async (fastify, _options) => { reply.code(200).send(sigMemCounts); }, ); + fastify.withTypeProvider().post( + "/addMember/:sigid", + { + schema: withRoles( + [AppRoles.SIGLEAD_MANAGER], + withTags(["Sigid"], { + // response: { + // 201: z.object({ + // id: z.string(), + // resource: z.string(), + // }), + // }, + body: postAddSigMemberSchema, + summary: "Add a member to a sig.", + }), + ) satisfies FastifyZodOpenApiSchema, + onRequest: fastify.authorizeFromSchema, + }, + async (request, reply) => { + const { sigGroupId, email, designation, memberName } = request.body; + const tableName = genericConfig.SigleadDynamoSigMemberTableName; + + // First try-catch: See if the member already exists + let sigMembers: SigMemberRecord[]; + try { + sigMembers = await fetchMemberRecords( + sigGroupId, + tableName, + fastify.dynamoClient, + ); + } catch (error) { + request.log.error( + `Could not verify the member does not already exist in the sig: ${error instanceof Error ? error.toString() : "Unknown error"}`, + ); + throw new DatabaseFetchError({ + message: "Failed to fetch sig member records from Dynamo table.", + }); + } + + for (const sigMember of sigMembers) { + if (sigMember.email === email) { + throw new ValidationError({ + message: "Member already exists in sig.", + }); + } + } + + const newMemberRecord: SigMemberRecord = request.body; + // Second try-catch: Try to add the member to Dynamo and AAD, rolling back if failure + try { + //FIXME: this is failing due to auth + const entraIdToken = await getEntraIdToken( + await getAuthorizedClients(), + fastify.environmentConfig.AadValidClientId, + ); + + await addMemberRecordToSig( + newMemberRecord, + tableName, + fastify.dynamoClient, + entraIdToken, + ); + } catch (error: any) { + request.log.error( + `Error while adding member to sig: ${error instanceof Error ? error.toString() : "Unknown error"}`, + ); + throw error; + } + + // Send the response + reply.code(200).send({ + message: "Added member to sig.", + }); + }, + ); }; fastify.register(limitedRoutes); diff --git a/src/common/types/siglead.ts b/src/common/types/siglead.ts index da642313..3b42c218 100644 --- a/src/common/types/siglead.ts +++ b/src/common/types/siglead.ts @@ -4,6 +4,13 @@ export type SigDetailRecord = { description: string; }; +export type SigEntraRecord = { + sigid: string; + signame: string; + leadGroupId: string; + memberGroupId: string; + }; + export type SigMemberRecord = { sigGroupId: string; email: string; diff --git a/src/ui/pages/siglead/ViewSigLead.page.tsx b/src/ui/pages/siglead/ViewSigLead.page.tsx index ca7af579..94480f34 100644 --- a/src/ui/pages/siglead/ViewSigLead.page.tsx +++ b/src/ui/pages/siglead/ViewSigLead.page.tsx @@ -13,6 +13,8 @@ import { Table, Group, Stack, + Modal, + Text, } from '@mantine/core'; import { DateTimePicker } from '@mantine/dates'; import { useForm, zodResolver } from '@mantine/form'; @@ -26,9 +28,13 @@ import { getRunEnvironmentConfig } from '@ui/config'; import { useApi } from '@ui/util/api'; import { AppRoles } from '@common/roles'; import { SigDetailRecord, SigMemberRecord } from '@common/types/siglead.js'; +import { IconCancel, IconCross, IconEdit, IconPlus, IconTrash } from '@tabler/icons-react'; +import { useDisclosure } from '@mantine/hooks'; export const ViewSigLeadPage: React.FC = () => { const [isSubmitting, setIsSubmitting] = useState(false); + const [isAddingMember, setIsAddingMember] = useState(false); + const [opened, { open, close }] = useDisclosure(false); const navigate = useNavigate(); const api = useApi('core'); const { colorScheme } = useMantineColorScheme(); @@ -41,6 +47,16 @@ export const ViewSigLeadPage: React.FC = () => { 'A cool Sig with a lot of money and members. Founded in 1999 by Sir Charlie of Edinburgh. Focuses on making money and helping others earn more money via education.', }); + const form = useForm({ + //validate: zodResolver(requestBodySchema), + initialValues: { + sigGroupId: sigId || '', + email: '', + designation: 'M', + memberName: '', + }, + }); + useEffect(() => { // Fetch sig data and populate form const getSig = async () => { @@ -91,53 +107,28 @@ export const ViewSigLeadPage: React.FC = () => { ); }; - /* - const form = useForm({ - validate: zodResolver(requestBodySchema), - initialValues: { - title: '', - description: '', - start: new Date(), - end: new Date(new Date().valueOf() + 3.6e6), // 1 hr later - location: 'ACM Room (Siebel CS 1104)', - locationLink: 'https://maps.app.goo.gl/dwbBBBkfjkgj8gvA8', - host: 'ACM', - featured: false, - repeats: undefined, - repeatEnds: undefined, - paidEventId: undefined, - }, - }); - /* - const handleSubmit = async (values: EventPostRequest) => { - try { - setIsSubmitting(true); - const realValues = { - ...values, - start: dayjs(values.start).format('YYYY-MM-DD[T]HH:mm:00'), - end: values.end ? dayjs(values.end).format('YYYY-MM-DD[T]HH:mm:00') : undefined, - repeatEnds: - values.repeatEnds && values.repeats - ? dayjs(values.repeatEnds).format('YYYY-MM-DD[T]HH:mm:00') - : undefined, - repeats: values.repeats ? values.repeats : undefined, - }; - - const eventURL = isEditing ? `/api/v1/events/${eventId}` : '/api/v1/events'; - const response = await api.post(eventURL, realValues); - notifications.show({ - title: isEditing ? 'Event updated!' : 'Event created!', - message: isEditing ? undefined : `The event ID is "${response.data.id}".`, - }); - navigate('/events/manage'); - } catch (error) { - setIsSubmitting(false); - console.error('Error creating/editing event:', error); - notifications.show({ - message: 'Failed to create/edit event, please try again.', - }); - } - };*/ + const handleSubmit = async (values: SigMemberRecord) => { + try { + setIsSubmitting(true); + + values.sigGroupId = sigDetails.sigid; + + const submitURL = `/api/v1/siglead/addMember/${sigDetails.sigid}`; + const response = await api.post(submitURL, values); + notifications.show({ + title: 'Member added!', + message: '', + }); + setIsAddingMember(false); + } catch (error: any) { + setIsSubmitting(false); + console.error('Error adding member:', error); + notifications.show({ + title: 'Failed to add member, please try again.', + message: error.response && error.response.data ? error.response.data.message : undefined, + }); + } + }; return ( @@ -151,7 +142,14 @@ export const ViewSigLeadPage: React.FC = () => { - +
+ { + setIsAddingMember(false); + close(); + }} + title={`Add Member to ${sigDetails.signame}`} + > +
+ + +
+ + + + + + +
); };