From a1d5d161ddd779d5057ba727d9ce65a4681b2756 Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Sun, 24 Jul 2022 00:26:38 -0700 Subject: [PATCH] Revamp backend code to support good local function development (#657) * Move concurrently dep upwards * Add express as explicit dependency * Accept just one HTTP method per endpoint * Fix endpoint option coalescing * Expressification of cloud functions * Nicer logging of API requests * Refactor web package.json * Add ts-node and nodemon to dev dependencies, bring back cors * Add scaffolding to point dev server at local functions * Enable emulator in dev server scaffolding * Fix up a little stuff I broke --- common/api.ts | 4 +- dev.sh | 45 +++++++++ functions/package.json | 4 + functions/src/api.ts | 63 ++++++------ functions/src/health.ts | 2 +- functions/src/index.ts | 79 +++++++++++---- functions/src/scripts/script-init.ts | 20 ++-- functions/src/serve.ts | 68 +++++++++++++ functions/src/stripe.ts | 27 ++--- functions/src/unsubscribe.ts | 110 +++++++++++---------- package.json | 5 +- web/package.json | 11 ++- yarn.lock | 143 +++++++++++++++++++++++++-- 13 files changed, 451 insertions(+), 130 deletions(-) create mode 100755 dev.sh create mode 100644 functions/src/serve.ts diff --git a/common/api.ts b/common/api.ts index b9376be5..1ae9a5fd 100644 --- a/common/api.ts +++ b/common/api.ts @@ -12,7 +12,9 @@ export class APIError extends Error { } export function getFunctionUrl(name: string) { - if (process.env.NEXT_PUBLIC_FIREBASE_EMULATE) { + if (process.env.NEXT_PUBLIC_FUNCTIONS_URL) { + return `${process.env.NEXT_PUBLIC_FUNCTIONS_URL}/${name}` + } else if (process.env.NEXT_PUBLIC_FIREBASE_EMULATE) { const { projectId, region } = ENV_CONFIG.firebaseConfig return `http://localhost:5001/${projectId}/${region}/${name}` } else { diff --git a/dev.sh b/dev.sh new file mode 100755 index 00000000..178e20e8 --- /dev/null +++ b/dev.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +ENV=${1:-dev} +case $ENV in + dev) + FIREBASE_PROJECT=dev + NEXT_ENV=DEV + EMULATOR=false ;; + prod) + FIREBASE_PROJECT=prod + NEXT_ENV=PROD + EMULATOR=false ;; + localdb) + FIREBASE_PROJECT=dev + NEXT_ENV=DEV + EMULATOR=true ;; + *) + echo "Invalid environment; must be dev, prod, or localdb." + exit 1 +esac + +firebase use $FIREBASE_PROJECT + +if [ ! -z $EMULATOR ] +then + npx concurrently \ + -n FIRESTORE,FUNCTIONS,NEXT,TS \ + -c green,white,magenta,cyan \ + "yarn --cwd=functions firestore" \ + "cross-env FIRESTORE_EMULATOR_HOST=localhost:8080 yarn --cwd=functions dev" \ + "cross-env NEXT_PUBLIC_FUNCTIONS_URL=http://localhost:8088 \ + NEXT_PUBLIC_FIREBASE_EMULATE=TRUE \ + NEXT_PUBLIC_FIREBASE_ENV=${NEXT_ENV} \ + yarn --cwd=web serve" \ + "cross-env yarn --cwd=web ts-watch" +else + npx concurrently \ + -n FUNCTIONS,NEXT,TS \ + -c white,magenta,cyan \ + "yarn --cwd=functions dev" \ + "cross-env NEXT_PUBLIC_FUNCTIONS_URL=http://localhost:8088 \ + NEXT_PUBLIC_FIREBASE_ENV=${NEXT_ENV} \ + yarn --cwd=web serve" \ + "cross-env yarn --cwd=web ts-watch" +fi diff --git a/functions/package.json b/functions/package.json index fe63dc4e..b20a8fd0 100644 --- a/functions/package.json +++ b/functions/package.json @@ -12,6 +12,8 @@ "start": "yarn shell", "deploy": "firebase deploy --only functions", "logs": "firebase functions:log", + "dev": "nodemon src/serve.ts", + "firestore": "firebase emulators:start --only firestore --import=./firestore_export", "serve": "firebase use dev && yarn build && firebase emulators:start --only functions,firestore --import=./firestore_export", "db:update-local-from-remote": "yarn db:backup-remote && gsutil rsync -r gs://$npm_package_config_firestore/firestore_export ./firestore_export", "db:backup-local": "firebase emulators:export --force ./firestore_export", @@ -29,6 +31,8 @@ "@tiptap/extension-link": "2.0.0-beta.43", "@tiptap/extension-mention": "2.0.0-beta.102", "@tiptap/starter-kit": "2.0.0-beta.190", + "cors": "2.8.5", + "express": "4.18.1", "firebase-admin": "10.0.0", "firebase-functions": "3.21.2", "lodash": "4.17.21", diff --git a/functions/src/api.ts b/functions/src/api.ts index 8c01ea05..fdda0ad5 100644 --- a/functions/src/api.ts +++ b/functions/src/api.ts @@ -1,6 +1,7 @@ import * as admin from 'firebase-admin' -import { logger } from 'firebase-functions/v2' -import { HttpsOptions, onRequest, Request } from 'firebase-functions/v2/https' +import { Request, RequestHandler, Response } from 'express' +import { error } from 'firebase-functions/logger' +import { HttpsOptions } from 'firebase-functions/v2/https' import { log } from './utils' import { z } from 'zod' import { APIError } from '../../common/api' @@ -45,7 +46,7 @@ export const parseCredentials = async (req: Request): Promise => { return { kind: 'jwt', data: await auth.verifyIdToken(payload) } } catch (err) { // This is somewhat suspicious, so get it into the firebase console - logger.error('Error verifying Firebase JWT: ', err) + error('Error verifying Firebase JWT: ', err) throw new APIError(403, 'Error validating token.') } case 'Key': @@ -83,6 +84,11 @@ export const zTimestamp = () => { }, z.date()) } +export type EndpointDefinition = { + opts: EndpointOptions & { method: string } + handler: RequestHandler +} + export const validate = (schema: T, val: unknown) => { const result = schema.safeParse(val) if (!result.success) { @@ -99,12 +105,12 @@ export const validate = (schema: T, val: unknown) => { } } -interface EndpointOptions extends HttpsOptions { - methods?: string[] +export interface EndpointOptions extends HttpsOptions { + method?: string } const DEFAULT_OPTS = { - methods: ['POST'], + method: 'POST', minInstances: 1, concurrency: 100, memory: '2GiB', @@ -113,28 +119,29 @@ const DEFAULT_OPTS = { } export const newEndpoint = (endpointOpts: EndpointOptions, fn: Handler) => { - const opts = Object.assign(endpointOpts, DEFAULT_OPTS) - return onRequest(opts, async (req, res) => { - log('Request processing started.') - try { - if (!opts.methods.includes(req.method)) { - const allowed = opts.methods.join(', ') - throw new APIError(405, `This endpoint supports only ${allowed}.`) - } - const authedUser = await lookupUser(await parseCredentials(req)) - log('User credentials processed.') - res.status(200).json(await fn(req, authedUser)) - } catch (e) { - if (e instanceof APIError) { - const output: { [k: string]: unknown } = { message: e.message } - if (e.details != null) { - output.details = e.details + const opts = Object.assign({}, DEFAULT_OPTS, endpointOpts) + return { + opts, + handler: async (req: Request, res: Response) => { + log(`${req.method} ${req.url} ${JSON.stringify(req.body)}`) + try { + if (opts.method !== req.method) { + throw new APIError(405, `This endpoint supports only ${opts.method}.`) + } + const authedUser = await lookupUser(await parseCredentials(req)) + res.status(200).json(await fn(req, authedUser)) + } catch (e) { + if (e instanceof APIError) { + const output: { [k: string]: unknown } = { message: e.message } + if (e.details != null) { + output.details = e.details + } + res.status(e.code).json(output) + } else { + error(e) + res.status(500).json({ message: 'An unknown error occurred.' }) } - res.status(e.code).json(output) - } else { - logger.error(e) - res.status(500).json({ message: 'An unknown error occurred.' }) } - } - }) + }, + } as EndpointDefinition } diff --git a/functions/src/health.ts b/functions/src/health.ts index 938261db..4ce04e05 100644 --- a/functions/src/health.ts +++ b/functions/src/health.ts @@ -1,6 +1,6 @@ import { newEndpoint } from './api' -export const health = newEndpoint({ methods: ['GET'] }, async (_req, auth) => { +export const health = newEndpoint({ method: 'GET' }, async (_req, auth) => { return { message: 'Server is working.', uid: auth.uid, diff --git a/functions/src/index.ts b/functions/src/index.ts index df311886..239806de 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -1,4 +1,6 @@ import * as admin from 'firebase-admin' +import { onRequest } from 'firebase-functions/v2/https' +import { EndpointDefinition } from './api' admin.initializeApp() @@ -25,20 +27,63 @@ export * from './on-delete-group' export * from './score-contracts' // v2 -export * from './health' -export * from './transact' -export * from './change-user-info' -export * from './create-user' -export * from './create-answer' -export * from './place-bet' -export * from './cancel-bet' -export * from './sell-bet' -export * from './sell-shares' -export * from './claim-manalink' -export * from './create-contract' -export * from './add-liquidity' -export * from './withdraw-liquidity' -export * from './create-group' -export * from './resolve-market' -export * from './unsubscribe' -export * from './stripe' +import { health } from './health' +import { transact } from './transact' +import { changeuserinfo } from './change-user-info' +import { createuser } from './create-user' +import { createanswer } from './create-answer' +import { placebet } from './place-bet' +import { cancelbet } from './cancel-bet' +import { sellbet } from './sell-bet' +import { sellshares } from './sell-shares' +import { claimmanalink } from './claim-manalink' +import { createmarket } from './create-contract' +import { addliquidity } from './add-liquidity' +import { withdrawliquidity } from './withdraw-liquidity' +import { creategroup } from './create-group' +import { resolvemarket } from './resolve-market' +import { unsubscribe } from './unsubscribe' +import { stripewebhook, createcheckoutsession } from './stripe' + +const toCloudFunction = ({ opts, handler }: EndpointDefinition) => { + return onRequest(opts, handler as any) +} +const healthFunction = toCloudFunction(health) +const transactFunction = toCloudFunction(transact) +const changeUserInfoFunction = toCloudFunction(changeuserinfo) +const createUserFunction = toCloudFunction(createuser) +const createAnswerFunction = toCloudFunction(createanswer) +const placeBetFunction = toCloudFunction(placebet) +const cancelBetFunction = toCloudFunction(cancelbet) +const sellBetFunction = toCloudFunction(sellbet) +const sellSharesFunction = toCloudFunction(sellshares) +const claimManalinkFunction = toCloudFunction(claimmanalink) +const createMarketFunction = toCloudFunction(createmarket) +const addLiquidityFunction = toCloudFunction(addliquidity) +const withdrawLiquidityFunction = toCloudFunction(withdrawliquidity) +const createGroupFunction = toCloudFunction(creategroup) +const resolveMarketFunction = toCloudFunction(resolvemarket) +const unsubscribeFunction = toCloudFunction(unsubscribe) +const stripeWebhookFunction = toCloudFunction(stripewebhook) +const createCheckoutSessionFunction = toCloudFunction(createcheckoutsession) + +export { + healthFunction as health, + transactFunction as transact, + changeUserInfoFunction as changeuserinfo, + createUserFunction as createuser, + createAnswerFunction as createanswer, + placeBetFunction as placebet, + cancelBetFunction as cancelbet, + sellBetFunction as sellbet, + sellSharesFunction as sellshares, + claimManalinkFunction as claimmanalink, + createMarketFunction as createmarket, + addLiquidityFunction as addliquidity, + withdrawLiquidityFunction as withdrawliquidity, + createGroupFunction as creategroup, + resolveMarketFunction as resolvemarket, + unsubscribeFunction as unsubscribe, + stripeWebhookFunction as stripewebhook, + createCheckoutSessionFunction as createcheckoutsession, +} diff --git a/functions/src/scripts/script-init.ts b/functions/src/scripts/script-init.ts index cc17a620..5f7dc410 100644 --- a/functions/src/scripts/script-init.ts +++ b/functions/src/scripts/script-init.ts @@ -66,10 +66,18 @@ export const getServiceAccountCredentials = (env?: string) => { } export const initAdmin = (env?: string) => { - const serviceAccount = getServiceAccountCredentials(env) - console.log(`Initializing connection to ${serviceAccount.project_id}...`) - return admin.initializeApp({ - projectId: serviceAccount.project_id, - credential: admin.credential.cert(serviceAccount), - }) + try { + const serviceAccount = getServiceAccountCredentials(env) + console.log( + `Initializing connection to ${serviceAccount.project_id} Firebase...` + ) + return admin.initializeApp({ + projectId: serviceAccount.project_id, + credential: admin.credential.cert(serviceAccount), + }) + } catch (err) { + console.error(err) + console.log(`Initializing connection to default Firebase...`) + return admin.initializeApp() + } } diff --git a/functions/src/serve.ts b/functions/src/serve.ts new file mode 100644 index 00000000..77282951 --- /dev/null +++ b/functions/src/serve.ts @@ -0,0 +1,68 @@ +import * as cors from 'cors' +import * as express from 'express' +import { Express, Request, Response, NextFunction } from 'express' +import { EndpointDefinition } from './api' + +const PORT = 8088 + +import { initAdmin } from './scripts/script-init' +initAdmin() + +import { health } from './health' +import { transact } from './transact' +import { changeuserinfo } from './change-user-info' +import { createuser } from './create-user' +import { createanswer } from './create-answer' +import { placebet } from './place-bet' +import { cancelbet } from './cancel-bet' +import { sellbet } from './sell-bet' +import { sellshares } from './sell-shares' +import { claimmanalink } from './claim-manalink' +import { createmarket } from './create-contract' +import { addliquidity } from './add-liquidity' +import { withdrawliquidity } from './withdraw-liquidity' +import { creategroup } from './create-group' +import { resolvemarket } from './resolve-market' +import { unsubscribe } from './unsubscribe' +import { stripewebhook, createcheckoutsession } from './stripe' + +type Middleware = (req: Request, res: Response, next: NextFunction) => void +const app = express() + +const addEndpointRoute = ( + path: string, + endpoint: EndpointDefinition, + ...middlewares: Middleware[] +) => { + const method = endpoint.opts.method.toLowerCase() as keyof Express + const corsMiddleware = cors({ origin: endpoint.opts.cors }) + const allMiddleware = [...middlewares, corsMiddleware] + app.options(path, corsMiddleware) // preflight requests + app[method](path, ...allMiddleware, endpoint.handler) +} + +const addJsonEndpointRoute = (name: string, endpoint: EndpointDefinition) => { + addEndpointRoute(name, endpoint, express.json()) +} + +addEndpointRoute('/health', health) +addJsonEndpointRoute('/transact', transact) +addJsonEndpointRoute('/changeuserinfo', changeuserinfo) +addJsonEndpointRoute('/createuser', createuser) +addJsonEndpointRoute('/createanswer', createanswer) +addJsonEndpointRoute('/placebet', placebet) +addJsonEndpointRoute('/cancelbet', cancelbet) +addJsonEndpointRoute('/sellbet', sellbet) +addJsonEndpointRoute('/sellshares', sellshares) +addJsonEndpointRoute('/claimmanalink', claimmanalink) +addJsonEndpointRoute('/createmarket', createmarket) +addJsonEndpointRoute('/addliquidity', addliquidity) +addJsonEndpointRoute('/withdrawliquidity', withdrawliquidity) +addJsonEndpointRoute('/creategroup', creategroup) +addJsonEndpointRoute('/resolvemarket', resolvemarket) +addJsonEndpointRoute('/unsubscribe', unsubscribe) +addJsonEndpointRoute('/createcheckoutsession', createcheckoutsession) +addEndpointRoute('/stripewebhook', stripewebhook, express.raw()) + +app.listen(PORT) +console.log(`Serving functions on port ${PORT}.`) diff --git a/functions/src/stripe.ts b/functions/src/stripe.ts index 450bbe35..79f0ad53 100644 --- a/functions/src/stripe.ts +++ b/functions/src/stripe.ts @@ -1,7 +1,7 @@ -import { onRequest } from 'firebase-functions/v2/https' import * as admin from 'firebase-admin' import Stripe from 'stripe' +import { EndpointDefinition } from './api' import { getPrivateUser, getUser, isProd, payUser } from './utils' import { sendThankYouEmail } from './emails' import { track } from './analytics' @@ -42,9 +42,9 @@ const manticDollarStripePrice = isProd() 10000: 'price_1K8bEiGdoFKoCJW7Us4UkRHE', } -export const createcheckoutsession = onRequest( - { minInstances: 1, secrets: ['STRIPE_APIKEY'] }, - async (req, res) => { +export const createcheckoutsession: EndpointDefinition = { + opts: { method: 'POST', minInstances: 1, secrets: ['STRIPE_APIKEY'] }, + handler: async (req, res) => { const userId = req.query.userId?.toString() const manticDollarQuantity = req.query.manticDollarQuantity?.toString() @@ -86,21 +86,24 @@ export const createcheckoutsession = onRequest( }) res.redirect(303, session.url || '') - } -) + }, +} -export const stripewebhook = onRequest( - { +export const stripewebhook: EndpointDefinition = { + opts: { + method: 'POST', minInstances: 1, secrets: ['MAILGUN_KEY', 'STRIPE_APIKEY', 'STRIPE_WEBHOOKSECRET'], }, - async (req, res) => { + handler: async (req, res) => { const stripe = initStripe() let event try { + // Cloud Functions jam the raw body into a special `rawBody` property + const rawBody = (req as any).rawBody ?? req.body event = stripe.webhooks.constructEvent( - req.rawBody, + rawBody, req.headers['stripe-signature'] as string, process.env.STRIPE_WEBHOOKSECRET as string ) @@ -116,8 +119,8 @@ export const stripewebhook = onRequest( } res.status(200).send('success') - } -) + }, +} const issueMoneys = async (session: StripeSession) => { const { id: sessionId } = session diff --git a/functions/src/unsubscribe.ts b/functions/src/unsubscribe.ts index 48dd29c0..fda20e16 100644 --- a/functions/src/unsubscribe.ts +++ b/functions/src/unsubscribe.ts @@ -1,66 +1,72 @@ -import { onRequest } from 'firebase-functions/v2/https' import * as admin from 'firebase-admin' +import { EndpointDefinition } from './api' import { getUser } from './utils' import { PrivateUser } from '../../common/user' -export const unsubscribe = onRequest({ minInstances: 1 }, async (req, res) => { - const id = req.query.id as string - let type = req.query.type as string - if (!id || !type) { - res.status(400).send('Empty id or type parameter.') - return - } +export const unsubscribe: EndpointDefinition = { + opts: { method: 'GET', minInstances: 1 }, + handler: async (req, res) => { + const id = req.query.id as string + let type = req.query.type as string + if (!id || !type) { + res.status(400).send('Empty id or type parameter.') + return + } - if (type === 'market-resolved') type = 'market-resolve' + if (type === 'market-resolved') type = 'market-resolve' - if ( - !['market-resolve', 'market-comment', 'market-answer', 'generic'].includes( - type - ) - ) { - res.status(400).send('Invalid type parameter.') - return - } + if ( + ![ + 'market-resolve', + 'market-comment', + 'market-answer', + 'generic', + ].includes(type) + ) { + res.status(400).send('Invalid type parameter.') + return + } - const user = await getUser(id) + const user = await getUser(id) - if (!user) { - res.send('This user is not currently subscribed or does not exist.') - return - } + if (!user) { + res.send('This user is not currently subscribed or does not exist.') + return + } - const { name } = user + const { name } = user - const update: Partial = { - ...(type === 'market-resolve' && { - unsubscribedFromResolutionEmails: true, - }), - ...(type === 'market-comment' && { - unsubscribedFromCommentEmails: true, - }), - ...(type === 'market-answer' && { - unsubscribedFromAnswerEmails: true, - }), - ...(type === 'generic' && { - unsubscribedFromGenericEmails: true, - }), - } + const update: Partial = { + ...(type === 'market-resolve' && { + unsubscribedFromResolutionEmails: true, + }), + ...(type === 'market-comment' && { + unsubscribedFromCommentEmails: true, + }), + ...(type === 'market-answer' && { + unsubscribedFromAnswerEmails: true, + }), + ...(type === 'generic' && { + unsubscribedFromGenericEmails: true, + }), + } - await firestore.collection('private-users').doc(id).update(update) + await firestore.collection('private-users').doc(id).update(update) - if (type === 'market-resolve') - res.send( - `${name}, you have been unsubscribed from market resolution emails on Manifold Markets.` - ) - else if (type === 'market-comment') - res.send( - `${name}, you have been unsubscribed from market comment emails on Manifold Markets.` - ) - else if (type === 'market-answer') - res.send( - `${name}, you have been unsubscribed from market answer emails on Manifold Markets.` - ) - else res.send(`${name}, you have been unsubscribed.`) -}) + if (type === 'market-resolve') + res.send( + `${name}, you have been unsubscribed from market resolution emails on Manifold Markets.` + ) + else if (type === 'market-comment') + res.send( + `${name}, you have been unsubscribed from market comment emails on Manifold Markets.` + ) + else if (type === 'market-answer') + res.send( + `${name}, you have been unsubscribed from market answer emails on Manifold Markets.` + ) + else res.send(`${name}, you have been unsubscribed.`) + }, +} const firestore = admin.firestore() diff --git a/package.json b/package.json index e4aee3fd..77420607 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,13 @@ "devDependencies": { "@typescript-eslint/eslint-plugin": "5.25.0", "@typescript-eslint/parser": "5.25.0", + "concurrently": "6.5.1", "eslint": "8.15.0", "eslint-plugin-lodash": "^7.4.0", "prettier": "2.5.0", - "typescript": "4.6.4" + "typescript": "4.6.4", + "ts-node": "10.9.1", + "nodemon": "2.0.19" }, "resolutions": { "@types/react": "17.0.43" diff --git a/web/package.json b/web/package.json index 9f27643e..a31dbffa 100644 --- a/web/package.json +++ b/web/package.json @@ -3,12 +3,14 @@ "version": "1.0.0", "private": true, "scripts": { - "dev": "concurrently -n NEXT,TS -c magenta,cyan \"next dev -p 3000\" \"yarn ts --watch\"", - "devdev": "cross-env NEXT_PUBLIC_FIREBASE_ENV=DEV concurrently -n NEXT,TS -c magenta,cyan \"cross-env FIREBASE_ENV=DEV next dev -p 3000\" \"cross-env FIREBASE_ENV=DEV yarn ts --watch\"", + "serve": "next dev -p 3000", + "ts-watch": "tsc --watch --noEmit --incremental --preserveWatchOutput --pretty", + "dev": "concurrently -n NEXT,TS -c magenta,cyan \"yarn serve\" \"yarn ts-watch\"", + "devdev": "cross-env NEXT_PUBLIC_FIREBASE_ENV=DEV yarn dev", "dev:dev": "yarn devdev", - "dev:the": "cross-env NEXT_PUBLIC_FIREBASE_ENV=THEOREMONE concurrently -n NEXT,TS -c magenta,cyan \"cross-env FIREBASE_ENV=THEOREMONE next dev -p 3000\" \"cross-env FIREBASE_ENV=THEOREMONE yarn ts --watch\"", + "dev:the": "cross-env NEXT_PUBLIC_FIREBASE_ENV=THEOREMONE yarn dev", + "dev:local": "cross-env NEXT_PUBLIC_FUNCTIONS_URL=http://localhost:8080 yarn devdev", "dev:emulate": "cross-env NEXT_PUBLIC_FIREBASE_EMULATE=TRUE yarn devdev", - "ts": "tsc --noEmit --incremental --preserveWatchOutput --pretty", "build": "next build", "start": "next start", "lint": "next lint", @@ -62,7 +64,6 @@ "@types/react": "17.0.43", "@types/string-similarity": "^4.0.0", "autoprefixer": "10.2.6", - "concurrently": "6.5.1", "critters": "0.0.16", "cross-env": "^7.0.3", "eslint-config-next": "12.1.6", diff --git a/yarn.lock b/yarn.lock index 019a3dd4..dea9e29b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1353,6 +1353,13 @@ resolved "https://registry.yarnpkg.com/@corex/deepmerge/-/deepmerge-2.6.148.tgz#8fa825d53ffd1cbcafce1b6a830eefd3dcc09dd5" integrity sha512-6QMz0/2h5C3ua51iAnXMPWFbb1QOU1UvSM4bKBw5mzdT+WtLgjbETBBIQZ+Sh9WvEcGwlAt/DEdRpIC3XlDBMA== +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + "@docsearch/css@3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.1.0.tgz#6781cad43fc2e034d012ee44beddf8f93ba21f19" @@ -2337,6 +2344,14 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz#b6461fb0c2964356c469e115f504c95ad97ab88c" integrity sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w== +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping@^0.3.9": version "0.3.13" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz#dcfe3e95f224c8fe97a87a5235defec999aa92ea" @@ -3106,6 +3121,26 @@ resolved "https://registry.yarnpkg.com/@tsconfig/docusaurus/-/docusaurus-1.0.5.tgz#5298c5b0333c6263f06c3149b38ebccc9f169a4e" integrity sha512-KM/TuJa9fugo67dTGx+ktIqf3fVc077J6jwHu845Hex4EQf7LABlNonP/mohDKT0cmncdtlYVHHF74xR/YpThg== +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" + integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== + "@types/body-parser@*": version "1.19.2" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" @@ -3670,7 +3705,7 @@ acorn-walk@^7.0.0: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== -acorn-walk@^8.0.0: +acorn-walk@^8.0.0, acorn-walk@^8.1.1: version "8.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== @@ -3854,6 +3889,11 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + arg@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.1.tgz#eb0c9a8f77786cad2af8ff2b862899842d7b6adb" @@ -4410,7 +4450,7 @@ cheerio@^1.0.0-rc.10: parse5-htmlparser2-tree-adapter "^7.0.0" tslib "^2.4.0" -chokidar@^3.4.2, chokidar@^3.5.3: +chokidar@^3.4.2, chokidar@^3.5.2, chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -4772,6 +4812,11 @@ cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: path-type "^4.0.0" yaml "^1.10.0" +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + critters@0.0.16: version "0.0.16" resolved "https://registry.yarnpkg.com/critters/-/critters-0.0.16.tgz#ffa2c5561a65b43c53b940036237ce72dcebfe93" @@ -5274,6 +5319,11 @@ didyoumean@^1.2.2: resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -5927,7 +5977,7 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" -express@^4.16.4, express@^4.17.1, express@^4.17.3: +express@4.18.1, express@^4.16.4, express@^4.17.1, express@^4.17.3: version "4.18.1" resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf" integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q== @@ -7059,6 +7109,11 @@ idb@3.0.2: resolved "https://registry.yarnpkg.com/idb/-/idb-3.0.2.tgz#c8e9122d5ddd40f13b60ae665e4862f8b13fa384" integrity sha512-+FLa/0sTXqyux0o6C+i2lOR0VoS60LU/jzUo5xjfY6+7sEEgy4Gz1O7yFBXvjd7N0NyIGWIRg8DcQSLEG+VSPw== +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== + ignore@^5.1.9, ignore@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" @@ -8083,6 +8138,11 @@ make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: dependencies: semver "^6.0.0" +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + markdown-escapes@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535" @@ -8434,7 +8494,23 @@ node-releases@^2.0.3: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.4.tgz#f38252370c43854dc48aa431c766c6c398f40476" integrity sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ== -nopt@1.0.10: +nodemon@2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.19.tgz#cac175f74b9cb8b57e770d47841995eebe4488bd" + integrity sha512-4pv1f2bMDj0Eeg/MhGqxrtveeQ5/G/UVe9iO6uTZzjnRluSA4PVWf8CW99LUPwGB3eNIA7zUFoP77YuI7hOc0A== + dependencies: + chokidar "^3.5.2" + debug "^3.2.7" + ignore-by-default "^1.0.1" + minimatch "^3.0.4" + pstree.remy "^1.1.8" + semver "^5.7.1" + simple-update-notifier "^1.0.7" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.5" + +nopt@1.0.10, nopt@~1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= @@ -9552,6 +9628,11 @@ pseudomap@^1.0.1: resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= +pstree.remy@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -10422,12 +10503,12 @@ semver-diff@^3.1.1: dependencies: semver "^6.3.0" -"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.6.0: +"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.6.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@7.0.0: +semver@7.0.0, semver@~7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== @@ -10574,6 +10655,13 @@ signal-exit@^3.0.2, signal-exit@^3.0.3: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +simple-update-notifier@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz#7edf75c5bdd04f88828d632f762b2bc32996a9cc" + integrity sha512-BBKgR84BJQJm6WjWFMHgLVuo61FBDSj1z/xSFUIozqO6wO7ii0JxCqlIud7Enr/+LhlbNI0whErq96P2qHNWew== + dependencies: + semver "~7.0.0" + sirv@^1.0.7: version "1.0.19" resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.19.tgz#1d73979b38c7fe91fcba49c85280daa9c2363b49" @@ -10937,7 +11025,7 @@ stylehacks@^5.1.0: browserslist "^4.16.6" postcss-selector-parser "^6.0.4" -supports-color@^5.3.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" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -11117,6 +11205,13 @@ totalist@^1.0.0: resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== +touch@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" + integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== + dependencies: + nopt "~1.0.10" + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -11142,6 +11237,25 @@ trough@^1.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== +ts-node@10.9.1: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + tsc-files@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/tsc-files/-/tsc-files-1.1.3.tgz#ef4cfcb7affc9b90577d707a879dc53bb105be83" @@ -11253,6 +11367,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undefsafe@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + unherit@^1.0.4: version "1.1.3" resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.3.tgz#6c9b503f2b41b262330c80e91c8614abdaa69c22" @@ -11511,6 +11630,11 @@ uuid@^8.0.0, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" @@ -11930,6 +12054,11 @@ yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"