diff --git a/.eslintrc b/.eslintrc index 37e59b2a..1cb1b272 100644 --- a/.eslintrc +++ b/.eslintrc @@ -3,7 +3,10 @@ "plugin:prettier/recommended", "plugin:@typescript-eslint/recommended" ], - "plugins": ["import"], + "plugins": [ + "import", + "prettier" + ], "rules": { "import/no-unresolved": "error", "import/extensions": [ @@ -32,27 +35,38 @@ }, "settings": { "import/parsers": { - "@typescript-eslint/parser": [".ts", ".tsx", ".js", ".jsx"] + "@typescript-eslint/parser": [ + ".ts", + ".tsx", + ".js", + ".jsx" + ] }, "import/resolver": { "typescript": { "alwaysTryTypes": true, "project": [ "src/api/tsconfig.json", // Path to tsconfig.json in src/api - "src/ui/tsconfig.json" // Path to tsconfig.json in src/ui + "src/ui/tsconfig.json" // Path to tsconfig.json in src/ui ] } } }, "overrides": [ { - "files": ["*.test.ts", "*.testdata.ts"], + "files": [ + "*.test.ts", + "*.testdata.ts" + ], "rules": { "@typescript-eslint/no-explicit-any": "off" } }, { - "files": ["src/ui/*", "src/ui/**/*"], + "files": [ + "src/ui/*", + "src/ui/**/*" + ], "rules": { "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-unused-vars": "off" diff --git a/src/api/functions/membership.ts b/src/api/functions/membership.ts index 211f0601..04129048 100644 --- a/src/api/functions/membership.ts +++ b/src/api/functions/membership.ts @@ -1,4 +1,5 @@ import { + ConditionalCheckFailedException, DynamoDBClient, PutItemCommand, QueryCommand, @@ -6,8 +7,9 @@ import { import { marshall } from "@aws-sdk/util-dynamodb"; import { genericConfig } from "common/config.js"; import { FastifyBaseLogger } from "fastify"; -import { isUserInGroup } from "./entraId.js"; +import { isUserInGroup, modifyGroup } from "./entraId.js"; import { EntraGroupError } from "common/errors/index.js"; +import { EntraGroupActions } from "common/types/iam.js"; export async function checkPaidMembership( endpoint: string, @@ -76,17 +78,69 @@ export async function checkPaidMembershipFromEntra( export async function setPaidMembershipInTable( netId: string, dynamoClient: DynamoDBClient, -): Promise { + actor: string = "core-api-queried", +): Promise<{ updated: boolean }> { const obj = { email: `${netId}@illinois.edu`, inserted_at: new Date().toISOString(), - inserted_by: "membership-api-queried", + inserted_by: actor, }; - await dynamoClient.send( - new PutItemCommand({ - TableName: genericConfig.MembershipTableName, - Item: marshall(obj), - }), + try { + await dynamoClient.send( + new PutItemCommand({ + TableName: genericConfig.MembershipTableName, + Item: marshall(obj), + ConditionExpression: "attribute_not_exists(email)", + }), + ); + return { updated: true }; + } catch (error: unknown) { + if (error instanceof ConditionalCheckFailedException) { + return { updated: false }; + } + throw error; + } +} + +type SetPaidMembershipInput = { + netId: string; + dynamoClient: DynamoDBClient; + entraToken: string; + paidMemberGroup: string; +}; + +type SetPaidMembershipOutput = { + updated: boolean; +}; + +export async function setPaidMembership({ + netId, + dynamoClient, + entraToken, + paidMemberGroup, +}: SetPaidMembershipInput): Promise { + const dynamoResult = await setPaidMembershipInTable( + netId, + dynamoClient, + "core-api-provisioned", ); + if (!dynamoResult.updated) { + const inEntra = await checkPaidMembershipFromEntra( + netId, + entraToken, + paidMemberGroup, + ); + if (inEntra) { + return { updated: false }; + } + } + await modifyGroup( + entraToken, + `${netId}@illinois.edu`, + paidMemberGroup, + EntraGroupActions.ADD, + ); + + return { updated: true }; } diff --git a/src/api/functions/stripe.ts b/src/api/functions/stripe.ts index 5078af86..178183d0 100644 --- a/src/api/functions/stripe.ts +++ b/src/api/functions/stripe.ts @@ -1,3 +1,4 @@ +import { InternalServerError } from "common/errors/index.js"; import Stripe from "stripe"; export type StripeLinkCreateParams = { @@ -9,6 +10,15 @@ export type StripeLinkCreateParams = { stripeApiKey: string; }; +export type StripeCheckoutSessionCreateParams = { + successUrl?: string; + returnUrl?: string; + customerEmail?: string; + stripeApiKey: string; + items: { price: string; quantity: number }[]; + initiator: string; +}; + /** * Create a Stripe payment link for an invoice. Note that invoiceAmountUsd MUST IN CENTS!! * @param {StripeLinkCreateParams} options @@ -53,3 +63,35 @@ export const createStripeLink = async ({ priceId: price.id, }; }; + +export const createCheckoutSession = async ({ + successUrl, + returnUrl, + stripeApiKey, + customerEmail, + items, + initiator, +}: StripeCheckoutSessionCreateParams): Promise => { + const stripe = new Stripe(stripeApiKey); + const payload: Stripe.Checkout.SessionCreateParams = { + success_url: successUrl || "", + cancel_url: returnUrl || "", + payment_method_types: ["card"], + line_items: items.map((item) => ({ + price: item.price, + quantity: item.quantity, + })), + mode: "payment", + customer_email: customerEmail, + metadata: { + initiator, + }, + }; + const session = await stripe.checkout.sessions.create(payload); + if (!session.url) { + throw new InternalServerError({ + message: "Could not create Stripe checkout session.", + }); + } + return session.url; +}; diff --git a/src/api/package.json b/src/api/package.json index 4c2f2507..7193ebe8 100644 --- a/src/api/package.json +++ b/src/api/package.json @@ -36,6 +36,7 @@ "esbuild": "^0.24.2", "fastify": "^5.1.0", "fastify-plugin": "^4.5.1", + "fastify-raw-body": "^5.0.0", "ical-generator": "^7.2.0", "jsonwebtoken": "^9.0.2", "jwks-rsa": "^3.1.0", diff --git a/src/api/routes/membership.ts b/src/api/routes/membership.ts index 1b24a095..eb6e2218 100644 --- a/src/api/routes/membership.ts +++ b/src/api/routes/membership.ts @@ -5,18 +5,33 @@ import { } from "api/functions/membership.js"; import { validateNetId } from "api/functions/validation.js"; import { FastifyPluginAsync } from "fastify"; -import { ValidationError } from "common/errors/index.js"; +import { + BaseError, + InternalServerError, + ValidationError, +} from "common/errors/index.js"; import { getEntraIdToken } from "api/functions/entraId.js"; import { genericConfig, roleArns } from "common/config.js"; import { getRoleCredentials } from "api/functions/sts.js"; import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import rateLimiter from "api/plugins/rateLimiter.js"; +import { createCheckoutSession } from "api/functions/stripe.js"; +import { getSecretValue } from "api/plugins/auth.js"; +import stripe, { Stripe } from "stripe"; +import { AvailableSQSFunctions, SQSPayload } from "common/types/sqsMessage.js"; +import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs"; +import rawbody, { RawBodyPluginOptions } from "fastify-raw-body"; const NONMEMBER_CACHE_SECONDS = 1800; // 30 minutes const MEMBER_CACHE_SECONDS = 43200; // 12 hours const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => { + await fastify.register(rawbody, { + field: "rawBody", + global: false, + runFirst: true, + }); const getAuthorizedClients = async () => { if (roleArns.Entra) { fastify.log.info( @@ -56,78 +71,223 @@ const membershipPlugin: FastifyPluginAsync = async (fastify, _options) => { fastify.get<{ Body: undefined; Querystring: { netId: string }; - }>( - "/:netId", - { - schema: { - querystring: { - type: "object", - properties: { - netId: { type: "string" }, - }, - }, - }, - }, - async (request, reply) => { - const netId = (request.params as Record).netId; - if (!validateNetId(netId)) { - throw new ValidationError({ - message: `${netId} is not a valid Illinois NetID!`, - }); + }>("/checkout/:netId", async (request, reply) => { + const netId = (request.params as Record).netId; + 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", + }), + ); + }); + fastify.get<{ + Body: undefined; + Querystring: { netId: string }; + }>("/:netId", async (request, reply) => { + const netId = (request.params as Record).netId; + if (!validateNetId(netId)) { + throw new ValidationError({ + message: `${netId} is not a valid Illinois NetID!`, + }); + } + if (fastify.nodeCache.get(`isMember_${netId}`) !== undefined) { + return reply.header("X-ACM-Data-Source", "cache").send({ + netId, + isPaidMember: fastify.nodeCache.get(`isMember_${netId}`), + }); + } + const isDynamoMember = await checkPaidMembershipFromTable( + netId, + fastify.dynamoClient, + ); + if (isDynamoMember) { + fastify.nodeCache.set(`isMember_${netId}`, 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}`, 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}`, + false, + NONMEMBER_CACHE_SECONDS, + ); + return reply + .header("X-ACM-Data-Source", "aad") + .send({ netId, isPaidMember: false }); + }); + }; + fastify.post( + "/provision", + { config: { rawBody: true } }, + async (request, reply) => { + let event: Stripe.Event; + if (!request.rawBody) { + throw new ValidationError({ message: "Could not get raw body." }); + } + try { + const sig = request.headers["stripe-signature"]; + if (!sig || typeof sig !== "string") { + throw new Error("Missing or invalid Stripe signature"); } - if (fastify.nodeCache.get(`isMember_${netId}`) !== undefined) { - return reply.header("X-ACM-Data-Source", "cache").send({ - netId, - isPaidMember: fastify.nodeCache.get(`isMember_${netId}`), + const secretApiConfig = + (await getSecretValue( + fastify.secretsManagerClient, + genericConfig.ConfigSecretName, + )) || {}; + if (!secretApiConfig) { + throw new InternalServerError({ + message: "Could not connect to Stripe.", }); } - const isDynamoMember = await checkPaidMembershipFromTable( - netId, - fastify.dynamoClient, + event = stripe.webhooks.constructEvent( + request.rawBody, + sig, + secretApiConfig.stripe_endpoint_secret as string, ); - if (isDynamoMember) { - fastify.nodeCache.set( - `isMember_${netId}`, - true, - MEMBER_CACHE_SECONDS, - ); - return reply - .header("X-ACM-Data-Source", "dynamo") - .send({ netId, isPaidMember: true }); + } catch (err: unknown) { + if (err instanceof BaseError) { + throw err; } - 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); - return; - } - fastify.nodeCache.set( - `isMember_${netId}`, - false, - NONMEMBER_CACHE_SECONDS, - ); - return reply - .header("X-ACM-Data-Source", "aad") - .send({ netId, isPaidMember: false }); - }, - ); - }; + throw new ValidationError({ + message: "Stripe webhook could not be validated.", + }); + } + switch (event.type) { + case "checkout.session.completed": + if ( + event.data.object.metadata && + "initiator" in event.data.object.metadata && + event.data.object.metadata["initiator"] == "purchase-membership" + ) { + const customerEmail = event.data.object.customer_email; + if (!customerEmail) { + return reply + .code(200) + .send({ handled: false, requestId: request.id }); + } + const sqsPayload: SQSPayload = + { + function: AvailableSQSFunctions.ProvisionNewMember, + metadata: { + initiator: event.id, + reqId: request.id, + }, + payload: { + email: customerEmail, + }, + }; + if (!fastify.sqsClient) { + fastify.sqsClient = new SQSClient({ + region: genericConfig.AwsRegion, + }); + } + const result = await fastify.sqsClient.send( + new SendMessageCommand({ + QueueUrl: fastify.environmentConfig.SqsQueueUrl, + MessageBody: JSON.stringify(sqsPayload), + }), + ); + if (!result.MessageId) { + request.log.error(result); + throw new InternalServerError({ + message: "Could not add job to queue.", + }); + } + return reply.status(200).send({ + handled: true, + requestId: request.id, + queueId: result.MessageId, + }); + } else { + return reply + .code(200) + .send({ handled: false, requestId: request.id }); + } + default: + request.log.warn(`Unhandled event type: ${event.type}`); + } + return reply.code(200).send({ handled: false, requestId: request.id }); + }, + ); fastify.register(limitedRoutes); }; diff --git a/src/api/sqs/handlers.ts b/src/api/sqs/handlers.ts index 43acec4f..13b864f3 100644 --- a/src/api/sqs/handlers.ts +++ b/src/api/sqs/handlers.ts @@ -20,6 +20,7 @@ import { generateMembershipEmailCommand } from "../../api/functions/ses.js"; 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"; const getAuthorizedClients = async ( logger: pino.Logger, @@ -84,7 +85,38 @@ export const emailMembershipPassHandler: SQSHandlerFunction< export const pingHandler: SQSHandlerFunction< AvailableSQSFunctions.Ping +> = async (_payload, _metadata, logger) => { + logger.info("Pong!"); +}; + +export const provisionNewMemberHandler: SQSHandlerFunction< + AvailableSQSFunctions.ProvisionNewMember > = async (payload, metadata, logger) => { - logger.error("Not implemented yet!"); - return; + const { email } = payload; + const commonConfig = { region: genericConfig.AwsRegion }; + const clients = await getAuthorizedClients(logger, commonConfig); + const entraToken = await getEntraIdToken( + clients, + currentEnvironmentConfig.AadValidClientId, + ); + logger.info("Got authorized clients and Entra ID token."); + const { updated } = await setPaidMembership({ + netId: email.replace("@illinois.edu", ""), + dynamoClient: clients.dynamoClient, + entraToken, + paidMemberGroup: currentEnvironmentConfig.PaidMemberGroupId, + }); + if (updated) { + logger.info( + { type: "audit", actor: metadata.initiator, target: email }, + "marked user as a paid member.", + ); + logger.info( + `${email} added as a paid member. Emailing their membership pass.`, + ); + + await emailMembershipPassHandler(payload, metadata, logger); + } else { + logger.info(`${email} was already a paid member.`); + } }; diff --git a/src/api/sqs/index.ts b/src/api/sqs/index.ts index 890697e7..5e8ac20d 100644 --- a/src/api/sqs/index.ts +++ b/src/api/sqs/index.ts @@ -12,7 +12,11 @@ import { import { logger } from "./logger.js"; import { z, ZodError } from "zod"; import pino from "pino"; -import { emailMembershipPassHandler, pingHandler } from "./handlers.js"; +import { + emailMembershipPassHandler, + pingHandler, + provisionNewMemberHandler, +} from "./handlers.js"; import { ValidationError } from "../../common/errors/index.js"; import { RunEnvironment } from "../../common/roles.js"; import { environmentConfig } from "../../common/config.js"; @@ -30,6 +34,7 @@ export type SQSHandlerFunction = ( const handlers: SQSFunctionPayloadTypes = { [AvailableSQSFunctions.EmailMembershipPass]: emailMembershipPassHandler, [AvailableSQSFunctions.Ping]: pingHandler, + [AvailableSQSFunctions.ProvisionNewMember]: provisionNewMemberHandler, }; export const runEnvironment = process.env.RunEnvironment as RunEnvironment; export const currentEnvironmentConfig = environmentConfig[runEnvironment]; diff --git a/src/common/config.ts b/src/common/config.ts index af6127a3..e4324dac 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -18,6 +18,7 @@ export type ConfigType = { EmailDomain: string; SqsQueueUrl: string; PaidMemberGroupId: string; + PaidMemberPriceId: string; }; export type GenericConfigType = { @@ -89,6 +90,7 @@ const environmentConfig: EnvironmentConfigType = { SqsQueueUrl: "https://sqs.us-east-1.amazonaws.com/427040638965/infra-core-api-sqs", PaidMemberGroupId: "9222451f-b354-4e64-ba28-c0f367a277c2", + PaidMemberPriceId: "price_1R4TcTDGHrJxx3mKI6XF9cNG", }, prod: { AzureRoleMapping: { AutonomousWriters: [AppRoles.EVENTS_MANAGER] }, @@ -107,6 +109,7 @@ const environmentConfig: EnvironmentConfigType = { SqsQueueUrl: "https://sqs.us-east-1.amazonaws.com/298118738376/infra-core-api-sqs", PaidMemberGroupId: "172fd9ee-69f0-4384-9786-41ff1a43cf8e", + PaidMemberPriceId: "price_1MUGIRDiGOXU9RuSChPYK6wZ", }, }; @@ -120,6 +123,7 @@ export type SecretConfig = { acm_passkit_signerKey_base64: string; apple_signing_cert_base64: string; stripe_secret_key: string; + stripe_endpoint_secret: string; }; const roleArns = { diff --git a/src/common/types/sqsMessage.ts b/src/common/types/sqsMessage.ts index 524d9853..0d4a4151 100644 --- a/src/common/types/sqsMessage.ts +++ b/src/common/types/sqsMessage.ts @@ -3,6 +3,7 @@ import { z, ZodError, ZodType } from "zod"; export enum AvailableSQSFunctions { Ping = "ping", EmailMembershipPass = "emailMembershipPass", + ProvisionNewMember = "provisionNewMember", } const sqsMessageMetadataSchema = z.object({ @@ -16,9 +17,12 @@ const baseSchema = z.object({ metadata: sqsMessageMetadataSchema, }); -const createSQSSchema = >( +const createSQSSchema = < + T extends AvailableSQSFunctions, + P extends ZodType, +>( func: T, - payloadSchema: P + payloadSchema: P, ) => baseSchema.extend({ function: z.literal(func), @@ -26,21 +30,25 @@ const createSQSSchema = }); export const sqsPayloadSchemas = { - [AvailableSQSFunctions.Ping]: createSQSSchema(AvailableSQSFunctions.Ping, z.object({})), + [AvailableSQSFunctions.Ping]: createSQSSchema( + AvailableSQSFunctions.Ping, + z.object({}), + ), [AvailableSQSFunctions.EmailMembershipPass]: createSQSSchema( AvailableSQSFunctions.EmailMembershipPass, - z.object({ email: z.string().email() }) + z.object({ email: z.string().email() }), + ), + [AvailableSQSFunctions.ProvisionNewMember]: createSQSSchema( + AvailableSQSFunctions.ProvisionNewMember, + z.object({ email: z.string().email() }), ), } as const; -export const sqsPayloadSchema = z.discriminatedUnion( - "function", - [ - sqsPayloadSchemas[AvailableSQSFunctions.Ping], - sqsPayloadSchemas[AvailableSQSFunctions.EmailMembershipPass], - ] as const -); - +export const sqsPayloadSchema = z.discriminatedUnion("function", [ + sqsPayloadSchemas[AvailableSQSFunctions.Ping], + sqsPayloadSchemas[AvailableSQSFunctions.EmailMembershipPass], + sqsPayloadSchemas[AvailableSQSFunctions.ProvisionNewMember], +] as const); export type SQSPayload = z.infer< (typeof sqsPayloadSchemas)[T] diff --git a/yarn.lock b/yarn.lock index 7bdec920..2bbaf894 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4131,6 +4131,11 @@ bytes@3.0.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + cac@^6.7.14: version "6.7.14" resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" @@ -4665,6 +4670,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + dequal@^2.0.2, dequal@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" @@ -5604,6 +5614,15 @@ fastify-plugin@^5.0.0: resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-5.0.1.tgz#82d44e6fe34d1420bb5a4f7bee434d501e41939f" integrity sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ== +fastify-raw-body@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/fastify-raw-body/-/fastify-raw-body-5.0.0.tgz#29d653c9af5605af9c937b4552b4838dfc99e0a7" + integrity sha512-2qfoaQ3BQDhZ1gtbkKZd6n0kKxJISJGM6u/skD9ljdWItAscjXrtZ1lnjr7PavmXX9j4EyCPmBDiIsLn07d5vA== + dependencies: + fastify-plugin "^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" @@ -6108,6 +6127,17 @@ html5-qrcode@^2.3.8: resolved "https://registry.yarnpkg.com/html5-qrcode/-/html5-qrcode-2.3.8.tgz#0b0cdf7a9926cfd4be530e13a51db47592adfa0d" integrity sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ== +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + http-proxy-agent@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" @@ -6222,7 +6252,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -7907,6 +7937,16 @@ range-parser@1.2.0: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" integrity sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A== +raw-body@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.0.tgz#25b3476f07a51600619dae3fe82ddc28a36e5e0f" + integrity sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.6.3" + unpipe "1.0.0" + rc@^1.0.1, rc@^1.1.6: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -8405,6 +8445,11 @@ scheduler@^0.23.2: dependencies: loose-envify "^1.1.0" +secure-json-parse@^2.4.0: + version "2.7.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" @@ -8491,6 +8536,11 @@ set-proto@^1.0.0: es-errors "^1.3.0" es-object-atoms "^1.0.0" +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -8683,6 +8733,11 @@ stackback@0.0.2: resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + std-env@^3.8.0: version "3.8.0" resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.8.0.tgz#b56ffc1baf1a29dcc80a3bdf11d7fca7c315e7d5" @@ -9192,6 +9247,11 @@ toad-cache@^3.7.0: resolved "https://registry.yarnpkg.com/toad-cache/-/toad-cache-3.7.0.tgz#b9b63304ea7c45ec34d91f1d2fa513517025c441" integrity sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw== +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + totalist@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8" @@ -9453,6 +9513,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== +unpipe@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + unplugin@^1.3.1: version "1.16.1" resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.16.1.tgz#a844d2e3c3b14a4ac2945c42be80409321b61199"