diff --git a/package.json b/package.json index b05cdbc..67af849 100644 --- a/package.json +++ b/package.json @@ -27,24 +27,28 @@ }, "homepage": "https://github.com/joshgachnang/mongoose-rest-framework#readme", "dependencies": { - "generaterr": "^1.5.0", - "passport-local": "^1.0.0", - "scmp": "^2.1.0", "@sentry/node": "^6.15.0", "axios": "^0.24.0", "cron": "^1.8.2", + "expo-server-sdk": "^3.6.0", "express": "^4.17.1", "express-session": "^1.17.2", + "generaterr": "^1.5.0", "jsonwebtoken": "^8.5.1", "lodash": "^4.17.21", - "mongoose": "^6.0.8", "on-finished": "^2.3.0", "passport": "^0.5.0", "passport-anonymous": "^1.0.1", "passport-jwt": "^4.0.0", + "passport-local": "^1.0.0", + "scmp": "^2.1.0", "winston": "^3.3.3" }, + "peerDependencies": { + "mongoose": "^6.1.9" + }, "devDependencies": { + "mongoose": "^6.1.9", "@types/bcrypt": "^5.0.0", "@types/chai": "^4.2.22", "@types/cron": "^1.7.3", @@ -58,7 +62,7 @@ "@types/passport": "^1.0.7", "@types/passport-anonymous": "^1.0.3", "@types/passport-jwt": "^3.0.6", - "@types/passport-local-mongoose": "^6.1.0", + "@types/passport-local": "^1.0.34", "@types/sinon": "^9.0.5", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^4.33.0", diff --git a/src/example.ts b/src/example.ts index b033473..655a261 100644 --- a/src/example.ts +++ b/src/example.ts @@ -1,7 +1,7 @@ import chai from "chai"; import express from "express"; import mongoose, {model, Schema} from "mongoose"; -import passportLocalMongoose from "passport-local-mongoose"; +import {passportLocalMongoose} from "./passport"; import {tokenPlugin} from "."; import { baseUserPlugin, diff --git a/src/expressServer.ts b/src/expressServer.ts index 4c31e9d..e2ba15c 100644 --- a/src/expressServer.ts +++ b/src/expressServer.ts @@ -2,11 +2,10 @@ import * as Sentry from "@sentry/node"; import axios from "axios"; import cron from "cron"; import express, {Router} from "express"; -import fs from "fs"; import cloneDeep from "lodash/cloneDeep"; import onFinished from "on-finished"; import passport from "passport"; -import winston, {level} from "winston"; +import {logger, LoggingOptions, setupLogging} from "./logger"; import {Env, setupAuth, UserModel} from "./mongooseRestFramework"; const SLOW_READ_MAX = 200; @@ -24,95 +23,6 @@ export function setupErrorLogging() { export type AddRoutes = (router: Router) => void; -// Setup a default console logger. -export const logger = winston.createLogger({ - level: "debug", - transports: [ - new winston.transports.Console({ - debugStdout: true, - level: "debug", - format: winston.format.combine(winston.format.colorize(), winston.format.simple()), - }), - ], -}); - -interface LoggingOptions { - level?: "debug" | "info" | "warn" | "error"; - transports?: winston.transport[]; - disableFileLogging?: boolean; - disableConsoleLogging?: boolean; - logDirectory?: string; -} - -function setupLogging(options?: LoggingOptions) { - logger.clear(); - if (!options?.disableConsoleLogging) { - logger.add( - new winston.transports.Console({ - debugStdout: !options?.level || options?.level === "debug", - level: options?.level ?? "debug", - format: winston.format.combine(winston.format.colorize(), winston.format.simple()), - }) - ); - } - if (!options?.disableFileLogging) { - const logDirectory = options?.logDirectory ?? "./log"; - if (!fs.existsSync(logDirectory)) { - fs.mkdirSync(logDirectory); - } - - const FILE_LOG_DEFAULTS = { - colorize: false, - compress: true, - dirname: logDirectory, - format: winston.format.simple(), - // 30 days of retention - maxFiles: 30, - // 50MB max file size - maxSize: 1024 * 1024 * 50, - // Only readable by server user - options: {mode: 0o600}, - }; - - logger.add( - new winston.transports.Stream({ - ...FILE_LOG_DEFAULTS, - level: "error", - handleExceptions: true, - // Use stream so we can open log in append mode rather than overwriting. - stream: fs.createWriteStream("error.log", {flags: "a"}), - }) - ); - - logger.add( - new winston.transports.Stream({ - ...FILE_LOG_DEFAULTS, - level: "info", - // Use stream so we can open log in append mode rather than overwriting. - stream: fs.createWriteStream("out.log", {flags: "a"}), - }) - ); - if (!options?.level || options?.level === "debug") { - logger.add( - new winston.transports.Stream({ - ...FILE_LOG_DEFAULTS, - level: "debug", - // Use stream so we can open log in append mode rather than overwriting. - stream: fs.createWriteStream("debug.log", {flags: "a"}), - }) - ); - } - } - - if (options?.transports) { - for (const transport of options.transports) { - logger.add(transport); - } - } - - logger.debug("Logger set up complete"); -} - const logRequestsFinished = function(req: any, res: any, startTime: [number, number]) { const diff = process.hrtime(startTime); const diffInMs = Math.round(diff[0] * 1000 + diff[1] * 0.000001); @@ -279,7 +189,7 @@ export function setupServer(options: SetupServerOptions) { return app; } -// Convenince method to execute cronjobs with an always-running server. +// Convenience method to execute cronjobs with an always-running server. export function cronjob( name: string, schedule: "hourly" | "minutely" | string, diff --git a/src/index.ts b/src/index.ts index 15069d8..e09c36f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,6 @@ export * from "./mongooseRestFramework"; export * from "./expressServer"; export * from "./passport"; +export * from "./models/messaging"; +export * from "./logger"; +export * from "./models/messaging"; diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..99516ab --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,91 @@ +import fs from "fs"; +import winston from "winston"; + +// Setup a default console logger. +export const logger = winston.createLogger({ + level: "debug", + transports: [ + new winston.transports.Console({ + debugStdout: true, + level: "debug", + format: winston.format.combine(winston.format.colorize(), winston.format.simple()), + }), + ], +}); + +export interface LoggingOptions { + level?: "debug" | "info" | "warn" | "error"; + transports?: winston.transport[]; + disableFileLogging?: boolean; + disableConsoleLogging?: boolean; + logDirectory?: string; +} + +export function setupLogging(options?: LoggingOptions) { + logger.clear(); + if (!options?.disableConsoleLogging) { + logger.add( + new winston.transports.Console({ + debugStdout: !options?.level || options?.level === "debug", + level: options?.level ?? "debug", + format: winston.format.combine(winston.format.colorize(), winston.format.simple()), + }) + ); + } + if (!options?.disableFileLogging) { + const logDirectory = options?.logDirectory ?? "./log"; + if (!fs.existsSync(logDirectory)) { + fs.mkdirSync(logDirectory); + } + + const FILE_LOG_DEFAULTS = { + colorize: false, + compress: true, + dirname: logDirectory, + format: winston.format.simple(), + // 30 days of retention + maxFiles: 30, + // 50MB max file size + maxSize: 1024 * 1024 * 50, + // Only readable by server user + options: {mode: 0o600}, + }; + + logger.add( + new winston.transports.Stream({ + ...FILE_LOG_DEFAULTS, + level: "error", + handleExceptions: true, + // Use stream so we can open log in append mode rather than overwriting. + stream: fs.createWriteStream("error.log", {flags: "a"}), + }) + ); + + logger.add( + new winston.transports.Stream({ + ...FILE_LOG_DEFAULTS, + level: "info", + // Use stream so we can open log in append mode rather than overwriting. + stream: fs.createWriteStream("out.log", {flags: "a"}), + }) + ); + if (!options?.level || options?.level === "debug") { + logger.add( + new winston.transports.Stream({ + ...FILE_LOG_DEFAULTS, + level: "debug", + // Use stream so we can open log in append mode rather than overwriting. + stream: fs.createWriteStream("debug.log", {flags: "a"}), + }) + ); + } + } + + if (options?.transports) { + for (const transport of options.transports) { + logger.add(transport); + } + } + + logger.debug("Logger set up complete"); +} diff --git a/src/models/interfaces.ts b/src/models/interfaces.ts new file mode 100644 index 0000000..130576b --- /dev/null +++ b/src/models/interfaces.ts @@ -0,0 +1,374 @@ +/* tslint:disable */ +/* eslint-disable */ + +import mongoose, {Types} from "mongoose"; + +// TODO: figure out how to make this work better. Generics? +type User = any; +type UserDocument = any; + +type StringOrObjectId = string | Types.ObjectId; + +export interface MessageData { + text: string; + from: StringOrObjectId; + conversationId: StringOrObjectId; +} + +export interface ConversationUser { + _id: StringOrObjectId +} + +/** + * Lean version of MessagePushStatusDocument + * + * This has all Mongoose getters & functions removed. This type will be returned from `MessageDocument.toObject()`. + * ``` + * const messageObject = message.toObject(); + * ``` + */ +export type MessagePushStatus = { + userId?: User["_id"] | User; + ticketStatus?: "ok" | "error"; + ticketId?: string; + ticketErrorMessage?: string; + ticketErrorType?: + | "DeviceNotRegistered" + | "InvalidCredentials" + | "MessageTooBig" + | "MessageRateExceeded"; + receiptStatus?: "ok" | "error"; + receiptErrorMessage?: string; + receiptErrorDetails?: string; + _id: mongoose.Types.ObjectId; +}; + +/** + * Lean version of MessageDocument + * + * This has all Mongoose getters & functions removed. This type will be returned from `MessageDocument.toObject()`. To avoid conflicts with model names, use the type alias `MessageObject`. + * ``` + * const messageObject = message.toObject(); + * ``` + */ +export type Message = { + text?: string; + from?: User["_id"] | User; + conversationId: Conversation["_id"] | Conversation; + pushStatuses: MessagePushStatus[]; + _id: mongoose.Types.ObjectId; +}; + +/** + * Lean version of MessageDocument (type alias of `Message`) + * + * Use this type alias to avoid conflicts with model names: + * ``` + * import { Message } from "../models" + * import { MessageObject } from "../interfaces/mongoose.gen.ts" + * + * const messageObject: MessageObject = message.toObject(); + * ``` + */ +export type MessageObject = Message; + +/** + * Mongoose Query types + * + * Pass this type to the Mongoose Model constructor: + * ``` + * const Message = mongoose.model("Message", MessageSchema); + * ``` + */ +export type MessageQueries = {}; + +export type MessageMethods = { + updatePushReceipts: (this: MessageDocument, backoffIndex?: number) => Promise; +}; + +export type MessageStatics = { + createFromMessageData: (this: MessageModel, messageData: MessageData) => Promise; +}; + +/** + * Mongoose Model type + * + * Pass this type to the Mongoose Model constructor: + * ``` + * const Message = mongoose.model("Message", MessageSchema); + * ``` + */ +export type MessageModel = mongoose.Model & MessageStatics; + +/** + * Mongoose Schema type + * + * Assign this type to new Message schema instances: + * ``` + * const MessageSchema: MessageSchema = new mongoose.Schema({ ... }) + * ``` + */ +export type MessageSchema = mongoose.Schema; + +/** + * Mongoose Subdocument type + * + * Type of `MessageDocument["pushStatuses"]` element. + */ +export type MessagePushStatusDocument = mongoose.Types.Subdocument & { + userId?: UserDocument["_id"] | UserDocument; + ticketStatus?: "ok" | "error"; + ticketId?: string; + ticketErrorMessage?: string; + ticketErrorType?: + | "DeviceNotRegistered" + | "InvalidCredentials" + | "MessageTooBig" + | "MessageRateExceeded"; + receiptStatus?: "ok" | "error"; + receiptErrorMessage?: string; + receiptErrorDetails?: string; + _id: mongoose.Types.ObjectId; +}; + +/** + * Mongoose Document type + * + * Pass this type to the Mongoose Model constructor: + * ``` + * const Message = mongoose.model("Message", MessageSchema); + * ``` + */ +export type MessageDocument = mongoose.Document & + MessageMethods & { + text?: string; + from?: UserDocument["_id"] | UserDocument; + conversationId: ConversationDocument["_id"] | ConversationDocument; + pushStatuses: mongoose.Types.DocumentArray; + _id: mongoose.Types.ObjectId; + }; + +/** + * Lean version of ConversationMemberDocument + * + * This has all Mongoose getters & functions removed. This type will be returned from `ConversationDocument.toObject()`. + * ``` + * const conversationObject = conversation.toObject(); + * ``` + */ +export type ConversationMember = { + userId:mongoose.Types.ObjectId | User; + _id: mongoose.Types.ObjectId; +}; + +/** + * Lean version of ConversationDocument + * + * This has all Mongoose getters & functions removed. This type will be returned from `ConversationDocument.toObject()`. To avoid conflicts with model names, use the type alias `ConversationObject`. + * ``` + * const conversationObject = conversation.toObject(); + * ``` + */ +export type Conversation = { + members: ConversationMember[]; + _id: mongoose.Types.ObjectId; +}; + +/** + * Lean version of ConversationDocument (type alias of `Conversation`) + * + * Use this type alias to avoid conflicts with model names: + * ``` + * import { Conversation } from "../models" + * import { ConversationObject } from "../interfaces/mongoose.gen.ts" + * + * const conversationObject: ConversationObject = conversation.toObject(); + * ``` + */ +export type ConversationObject = Conversation; + +/** + * Mongoose Query types + * + * Pass this type to the Mongoose Model constructor: + * ``` + * const Conversation = mongoose.model("Conversation", ConversationSchema); + * ``` + */ +export type ConversationQueries = {}; + +export type ConversationMethods = { + sendMessage: (this: ConversationDocument, message: MessageDocument) => Promise; + sendPushNotifications: ( + this: ConversationDocument, + message: MessageDocument, + members: ConversationMember[] + ) => Promise; + addMember: (this: ConversationDocument, member: ConversationMember) => Promise; + removeMember: (this: ConversationDocument, member: ConversationMember) => Promise; +}; + +export type ConversationStatics = { + createConversationForUser: ( + this: ConversationModel, + user: ConversationUser, + extraData: any + ) => void; + getPushDataForMember: ( + this: ConversationModel, + message: MessageDocument, + member: any + ) => {to: string; sound: string; body: string; data: {withSome: string}}; + sendMessageToMember: ( + this: ConversationModel, + message: MessageDocument, + member: any + ) => Promise; +}; + +/** + * Mongoose Model type + * + * Pass this type to the Mongoose Model constructor: + * ``` + * const Conversation = mongoose.model("Conversation", ConversationSchema); + * ``` + */ +export type ConversationModel = mongoose.Model & + ConversationStatics; + +/** + * Mongoose Schema type + * + * Assign this type to new Conversation schema instances: + * ``` + * const ConversationSchema: ConversationSchema = new mongoose.Schema({ ... }) + * ``` + */ +export type ConversationSchema = mongoose.Schema; + +/** + * Mongoose Subdocument type + * + * Type of `ConversationDocument["members"]` element. + */ +export type ConversationMemberDocument = mongoose.Types.Subdocument & { + userId: UserDocument["_id"] | UserDocument; + _id: mongoose.Types.ObjectId; +}; + +/** + * Mongoose Document type + * + * Pass this type to the Mongoose Model constructor: + * ``` + * const Conversation = mongoose.model("Conversation", ConversationSchema); + * ``` + */ +export type ConversationDocument = mongoose.Document & + ConversationMethods & { + members: mongoose.Types.DocumentArray; + _id: mongoose.Types.ObjectId; + }; + +/** + * Check if a property on a document is populated: + * ``` + * import { IsPopulated } from "../interfaces/mongoose.gen.ts" + * + * if (IsPopulated) { ... } + * ``` + */ +export function IsPopulated(doc: T | mongoose.Types.ObjectId): doc is T { + return doc instanceof mongoose.Document; +} + +/** + * Helper type used by `PopulatedDocument`. Returns the parent property of a string + * representing a nested property (i.e. `friend.user` -> `friend`) + */ +type ParentProperty = T extends `${infer P}.${string}` ? P : never; + +/** + * Helper type used by `PopulatedDocument`. Returns the child property of a string + * representing a nested property (i.e. `friend.user` -> `user`). + */ +type ChildProperty = T extends `${string}.${infer C}` ? C : never; + +/** + * Helper type used by `PopulatedDocument`. Removes the `ObjectId` from the general union type generated + * for ref documents (i.e. `mongoose.Types.ObjectId | UserDocument` -> `UserDocument`) + */ +type PopulatedProperty = Omit & { + [ref in T]: Root[T] extends mongoose.Types.Array + ? mongoose.Types.Array> + : Exclude; +}; + +/** + * Populate properties on a document type: + * ``` + * import { PopulatedDocument } from "../interfaces/mongoose.gen.ts" + * + * function example(user: PopulatedDocument) { + * console.log(user.bestFriend._id) // typescript knows this is populated + * } + * ``` + */ +export type PopulatedDocument = T extends keyof DocType + ? PopulatedProperty + : ParentProperty extends keyof DocType + ? Omit> & { + [ref in ParentProperty]: DocType[ParentProperty] extends mongoose.Types.Array + ? mongoose.Types.Array< + ChildProperty extends keyof U + ? PopulatedProperty> + : PopulatedDocument> + > + : ChildProperty extends keyof DocType[ParentProperty] + ? PopulatedProperty], ChildProperty> + : PopulatedDocument], ChildProperty>; + } + : DocType; + +/** + * Helper types used by the populate overloads + */ +type Unarray = T extends Array ? U : T; +type Modify = Omit & R; + +/** + * Augment mongoose with Query.populate overloads + */ +declare module "mongoose" { + interface Query { + populate( + path: T, + select?: string | any, + model?: string | Model, + match?: any + ): Query< + ResultType extends Array + ? Array, T>> + : ResultType extends DocType + ? PopulatedDocument, T> + : ResultType, + DocType, + THelpers + > & + THelpers; + + populate( + options: Modify | Array + ): Query< + ResultType extends Array + ? Array, T>> + : ResultType extends DocType + ? PopulatedDocument, T> + : ResultType, + DocType, + THelpers + > & + THelpers; + } +} diff --git a/src/models/messaging.ts b/src/models/messaging.ts new file mode 100644 index 0000000..ad72710 --- /dev/null +++ b/src/models/messaging.ts @@ -0,0 +1,368 @@ +/** + * Contains the APIs and model plugins to send and receive messages. Currently, supports Twilio and + * Expo push notifications. + */ +import Expo, {ExpoPushErrorTicket, ExpoPushSuccessTicket, ExpoPushTicket} from "expo-server-sdk"; +import mongoose, {Document, Schema, Types} from "mongoose"; +import {logger} from "../logger"; +import { + ConversationDocument, + ConversationModel as GeneratedConversationModel, + ConversationSchema as GeneratedConversationSchema, + ConversationUser, + MessageData, + MessageDocument, + MessageModel, + MessagePushStatus, + MessageSchema, +} from "./interfaces"; +import axios from "axios"; + +// Selectively export from interfaces +export {MessageSchema, MessageModel, MessageDocument} from "./interfaces"; + +const expo = new Expo({accessToken: process.env.EXPO_ACCESS_TOKEN}); + +const BACKOFF_SECONDS = [5, 5, 5, 15, 30]; + +// TODO make these adjustable by the calling app. +const DEFAULT_USER_MODEL = "User"; +const DEFAULT_CONVERSATION_MODEL = "Conversation"; + +// const DEFAULT_MESSAGE_MODEL = "Message"; + +function isPopulated(field: any): boolean { + if (Array.isArray(field)) { + if (field.length === 0) { + return false; + } else { + return field[0]._bsontype === "ObjectId"; + } + } else { + return field._bsontype === "ObjectId"; + } +} + +function isExpoPushTicketSuccess(data: ExpoPushTicket): data is ExpoPushSuccessTicket { + return data.status === "ok"; +} + +function isExpoPushTicketError(data: ExpoPushTicket): data is ExpoPushErrorTicket { + return data.status === "error"; +} + +export function userMessagingPlugin(schema: Schema) { + schema.add({ + expoToken: {type: String}, + messagingMethods: { + push: {enabled: {type: Boolean, default: true}, optedOut: {type: Boolean, default: false}}, + sms: {enabled: {type: Boolean, default: true}, optedOut: {type: Boolean, default: false}}, + }, + conversations: [ + { + conversationId: { + type: Schema.Types.ObjectId, + ref: DEFAULT_CONVERSATION_MODEL, + required: true, + }, + }, + ], + }); +} + +export function messagePlugin(messageSchema: Schema) { + messageSchema.add({ + text: {type: String}, + // Not required, if not specified, shows up as a system message. Your app should handle this + // on the frontend. + from: { + type: Schema.Types.ObjectId, + ref: DEFAULT_USER_MODEL, + }, + conversationId: { + type: Schema.Types.ObjectId, + ref: DEFAULT_CONVERSATION_MODEL, + required: true, + }, + pushStatuses: [ + { + // Expo returns a push ticket which tells us whether the Expo servers have accepted our push message. + userId: { + type: Schema.Types.ObjectId, + ref: DEFAULT_USER_MODEL, + }, + ticketStatus: {type: String, enum: ["ok", "error"]}, + // When a ticket is successful, we get a ticket id for querying for push receipt. + ticketId: String, + // If there was an error communicating with Expo, that message and type will be storied here. + ticketErrorMessage: String, + ticketErrorType: { + type: String, + enum: [ + "DeviceNotRegistered", + "InvalidCredentials", + "MessageTooBig", + "MessageRateExceeded", + ], + }, + // Receipts come from the iOS and Google push servers and represent whether the push was actually delivered. + receiptStatus: {type: String, enum: ["ok", "error"]}, + receiptErrorMessage: String, + receiptErrorDetails: String, + }, + ], + // TODO: Add support for threading messages and replies. + }); + + messageSchema.methods = { + // Ask the Expo server for push receipts to see what the status from Google/Apple is for push. + async updatePushReceipts(backoffIndex: number = 1) { + logger.debug(`Updating push receipts for ${this._id}`); + const ids = this.pushStatuses + .map((s: MessagePushStatus) => { + if (s.ticketStatus === "ok" && s.ticketId && !s.receiptStatus) { + return s.ticketId; + } + return null; + }) + .filter((s: string | null) => s); + + // Get push receipts + const res = await axios.post("https://exp.host/--/api/v2/push/getReceipts", { + ids, + }); + + for (const ticketId of Object.keys(res.data.data)) { + const receipt = res.data.data[ticketId]; + const pushStatus = this.pushStatuses.find( + (s: MessagePushStatus) => s.ticketId === ticketId + ); + if (!pushStatus) { + logger.error( + `Could not update push status for ticketId ${ticketId} in message ${this._id}` + ); + continue; + } + pushStatus.receiptStatus = receipt.status; + if (receipt.status === "error") { + pushStatus.receiptErrorMessage = receipt.message; + pushStatus.receiptErrorDetails = receipt.details; + } + } + await this.save(); + // If we don't have all the receipts, we'll keep checking for one minute. After that, we should + // check with a background job of some sort. + let count = 0; + for (const status of this.pushStatuses) { + if (!status.receiptStatus) { + count += 1; + } + } + if (count > 0) { + if (backoffIndex >= BACKOFF_SECONDS.length) { + logger.warn( + `Missing ${count}/${this.pushStatuses.length} push receipts after` + ` 60s, giving up.` + ); + return; + } + setTimeout(() => this.updatePushReceipts(backoffIndex + 1), BACKOFF_SECONDS[backoffIndex]); + } + }, + }; + + messageSchema.statics = { + async createFromMessageData(messageData: MessageData): Promise { + return this.create({ + from: messageData.from, + text: messageData.text, + conversationId: messageData.conversationId, + }); + }, + }; +} + +interface ConversationMember { + _id: Types.ObjectId; + userId: Types.ObjectId | Document; +} + +export interface ConversationSchema extends GeneratedConversationSchema {} + +export interface ConversationModel extends GeneratedConversationModel { + onMemberAdded?: ( + this: ConversationModel, + doc: ConversationDocument, + member: ConversationMember + ) => Promise | void; + onMemberRemoved?: ( + this: ConversationModel, + doc: ConversationDocument, + member: ConversationMember + ) => Promise | void; +} + +export function conversationPlugin(conversationSchema: Schema) { + conversationSchema.add({ + members: [ + { + userId: { + type: Schema.Types.ObjectId, + ref: DEFAULT_USER_MODEL, + required: true, + }, + }, + ], + }); + + conversationSchema.methods = { + // Actually send the message. If the members have push tokens, sends the message via push. Can + // also send va SMS if enabled. This function should be called from a worker or not awaited from + // a request handler as it will try to update the push status for up to 1 minute. + async sendMessage(message: MessageDocument) { + if (!isPopulated(this.members)) { + await this.populate("members"); + await this.populate("members.userId"); + } + // const members = (this.members as ConversationMember[]).filter((m) => m._id !== message.from); + const members = this.members as ConversationMember[]; + + logger.debug(`Sending message ${message._id} to ${members.length} members`); + this._sendPushNotifications(message, members); + }, + + // Private method to perform the push notification sending. Call sendMessage instead. + async _sendPushNotifications(message: MessageDocument, members: ConversationMember[]) { + const pushNotificationData: any = []; + const pushMembers: ConversationMember[] = []; + for (const member of members) { + const data = this._getExpoPushDataForMember(message, member.userId); + if (data === null) { + continue; + } + pushNotificationData.push(data); + pushMembers.push(member); + } + let tickets: ExpoPushTicket[] = []; + try { + tickets = (await expo.sendPushNotificationsAsync(pushNotificationData)) as ExpoPushTicket[]; + } catch (error) { + logger.error("Error sending push notification to Expo: ", error); + return; + } + logger.debug(`Result from sending message ${message._id}: ${JSON.stringify(tickets)}`); + // Try to fetch push results right away. We'll follow up on this with retries. + for (let i = 0; i < pushMembers.length; i++) { + const member = pushMembers[i]; + const ticket: ExpoPushTicket = tickets[i]; + + if (isExpoPushTicketSuccess(ticket)) { + message.pushStatuses.push({ + userId: member.userId, + ticketStatus: ticket.status, + ticketId: ticket.id, + }); + } else if (isExpoPushTicketError(ticket)) { + message.pushStatuses.push({ + userId: member.userId, + ticketStatus: ticket.status, + ticketErrorMessage: ticket.message, + ticketErrorType: ticket.details?.error, + }); + } else { + logger.error(`Unknown push ticket status`, ticket, member); + } + } + + await message.updatePushReceipts(); + }, + + async addMember(member: ConversationMember) { + const Conversation: any = mongoose.model(DEFAULT_CONVERSATION_MODEL); + if (this.members.length >= 1000) { + throw new Error(`Conversations are limited to 1000 members.`); + } + for (const m of this.members) { + if (m.userId === member.userId) { + logger.warn(`Cannot add member for user ${member.userId}, already is a member`); + return; + } + } + this.members.push(member); + const User = mongoose.model(DEFAULT_USER_MODEL); + const user = await User.findById(member.userId); + if (!user) { + throw new Error(`Could not find user ${member.userId} to add to conversation.`); + } + const result = await this.save(); + + (user as any).conversations.push({conversationId: result.id}); + await user.save(); + + if (Conversation.onMemberAdded) { + await Conversation.onMemberAdded(this, member); + } + + return result; + }, + + async removeMember(member: ConversationMember) { + const Conversation: any = mongoose.model(DEFAULT_CONVERSATION_MODEL); + this.members.pull({userId: member.userId}); + + const User = mongoose.model(DEFAULT_USER_MODEL); + const user = await User.findById(member.userId); + if (!user) { + throw new Error(`Could not find user ${member.userId} to remove from conversation.`); + } + + const result = await this.save(); + (user as any).conversations.pull({conversationId: result.id}); + await user.save(); + + if (Conversation.onMemberRemoved) { + await Conversation.onMemberRemoved(this, member); + } + return result; + }, + + // Private method to build the data to send to Expo for a push notification. + _getExpoPushDataForMember(message: MessageDocument, member: any) { + const pushToken = member.expoToken; + + if (!pushToken) { + logger.debug(`Not sending message to ${member.id}, no expo token.`); + return null; + } + if (!Expo.isExpoPushToken(pushToken)) { + logger.error(`Not sending message to ${member.id}, invalid Expo push token: ${pushToken}`); + return null; + } + // TODO: come up with a good way to handle this with reasonable defaults. + // if (!member.messageMethods?.push?.enabled) { + // logger.debug(`Not sending message to ${member.id}, push is not enabled.`); + // return null; + // } + if (member.messageMethods?.push?.optedOut) { + logger.debug(`Not sending message to ${member.id}, opted out.`); + return null; + } + + return { + to: pushToken, + sound: "default", + body: "This is a test notification", + data: {withSome: "data"}, + }; + }, + }; + + conversationSchema.statics = { + createConversationForUser(user: ConversationUser, extraData: any) { + console.log("Creating conversation for user", user._id); + return this.create({ + members: [{userId: user._id}], + ...extraData, + }); + }, + }; +} diff --git a/src/mongooseRestFramework.test.ts b/src/mongooseRestFramework.test.ts index 7eb9b26..5cf4418 100644 --- a/src/mongooseRestFramework.test.ts +++ b/src/mongooseRestFramework.test.ts @@ -64,6 +64,17 @@ const schema = new Schema({ const FoodModel = model("Food", schema); +interface RequiredField { + name: string; + about?: string; +} + +const requiredSchema = new Schema({ + name: {type: String, required: true}, + about: String, +}); +const RequiredModel = model("Required", requiredSchema); + function getBaseServer(): Express { const app = express(); @@ -85,6 +96,21 @@ afterAll(() => { mongoose.connection.close(); }); +async function setupDb() { + await Promise.all([UserModel.deleteMany({}), FoodModel.deleteMany({})]); + const [notAdmin, admin] = await Promise.all([ + UserModel.create({email: "notAdmin@example.com"}), + UserModel.create({email: "admin@example.com", admin: true}), + ]); + await (notAdmin as any).setPassword("password"); + await notAdmin.save(); + + await (admin as any).setPassword("securePassword"); + await admin.save(); + + return [admin, notAdmin]; +} + describe("mongoose rest framework", () => { let server: supertest.SuperTest; let app: express.Application; @@ -103,18 +129,188 @@ describe("mongoose rest framework", () => { process.env = OLD_ENV; }); - describe("permissions", function() { + describe("pre and post hooks", function() { + let app: any; beforeEach(async function() { - await Promise.all([UserModel.deleteMany({}), FoodModel.deleteMany({})]); - const [notAdmin, admin] = await Promise.all([ - UserModel.create({email: "notAdmin@example.com"}), - UserModel.create({email: "admin@example.com", admin: true}), - ]); - await (notAdmin as any).setPassword("password"); - await notAdmin.save(); + await setupDb(); + app = getBaseServer(); + setupAuth(app, UserModel as any); + }); + + it("pre hooks change data", async function() { + let deleteCalled = false; + app.use( + "/food", + gooseRestRouter(FoodModel, { + permissions: { + list: [Permissions.IsAny], + create: [Permissions.IsAny], + read: [Permissions.IsAny], + update: [Permissions.IsAny], + delete: [Permissions.IsAny], + }, + preCreate: (data: any) => { + data.calories = 14; + return data; + }, + preUpdate: (data: any) => { + data.calories = 15; + return data; + }, + preDelete: (data: any) => { + deleteCalled = true; + return data; + }, + }) + ); + const server = supertest(app); + + let res = await server + .post("/food") + .send({ + name: "Broccoli", + calories: 15, + }) + .expect(201); + const broccoli = await FoodModel.findById(res.body.data._id); + if (!broccoli) { + throw new Error("Broccoli was not created"); + } + assert.equal(broccoli.name, "Broccoli"); + // Overwritten by the pre create hook + assert.equal(broccoli.calories, 14); + + res = await server + .patch(`/food/${broccoli._id}`) + .send({ + name: "Broccoli2", + }) + .expect(200); + assert.equal(res.body.data.name, "Broccoli2"); + // Updated by the pre update hook + assert.equal(res.body.data.calories, 15); + + await server.delete(`/food/${broccoli._id}`).expect(204); + assert.isTrue(deleteCalled); + }); + + it("pre hooks return null", async function() { + const notAdmin = await UserModel.findOne({email: "notAdmin@example.com"}); + const spinach = await FoodModel.create({ + name: "Spinach", + calories: 1, + created: new Date("2021-12-03T00:00:20.000Z"), + ownerId: (notAdmin as any)._id, + hidden: false, + source: { + name: "Brand", + }, + }); + + app.use( + "/food", + gooseRestRouter(FoodModel, { + permissions: { + list: [Permissions.IsAny], + create: [Permissions.IsAny], + read: [Permissions.IsAny], + update: [Permissions.IsAny], + delete: [Permissions.IsAny], + }, + preCreate: () => null, + preUpdate: () => null, + preDelete: () => null, + }) + ); + const server = supertest(app); + + const res = await server + .post("/food") + .send({ + name: "Broccoli", + calories: 15, + }) + .expect(403); + const broccoli = await FoodModel.findById(res.body._id); + assert.isNull(broccoli); + + await server + .patch(`/food/${spinach._id}`) + .send({ + name: "Broccoli", + }) + .expect(403); - await (admin as any).setPassword("securePassword"); - await admin.save(); + await server.delete(`/food/${spinach._id}`).expect(403); + }); + + it("post hooks succeed", async function() { + let deleteCalled = false; + app.use( + "/food", + gooseRestRouter(FoodModel, { + permissions: { + list: [Permissions.IsAny], + create: [Permissions.IsAny], + read: [Permissions.IsAny], + update: [Permissions.IsAny], + delete: [Permissions.IsAny], + }, + postCreate: async (data: any) => { + data.calories = 14; + await data.save(); + return data; + }, + postUpdate: async (data: any) => { + data.calories = 15; + await data.save(); + return data; + }, + postDelete: (data: any) => { + deleteCalled = true; + return data; + }, + }) + ); + const server = supertest(app); + + let res = await server + .post("/food") + .send({ + name: "Broccoli", + calories: 15, + }) + .expect(201); + let broccoli = await FoodModel.findById(res.body.data._id); + if (!broccoli) { + throw new Error("Broccoli was not created"); + } + assert.equal(broccoli.name, "Broccoli"); + // Overwritten by the pre create hook + assert.equal(broccoli.calories, 14); + + res = await server + .patch(`/food/${broccoli._id}`) + .send({ + name: "Broccoli2", + }) + .expect(200); + broccoli = await FoodModel.findById(res.body.data._id); + if (!broccoli) { + throw new Error("Broccoli was not update"); + } + assert.equal(broccoli.name, "Broccoli2"); + // Updated by the post update hook + assert.equal(broccoli.calories, 15); + + await server.delete(`/food/${broccoli._id}`).expect(204); + assert.isTrue(deleteCalled); + }); + }); + + describe("permissions", function() { + beforeEach(async function() { + const [admin, notAdmin] = await setupDb(); await Promise.all([ FoodModel.create({ @@ -144,6 +340,18 @@ describe("mongoose rest framework", () => { }, }) ); + app.use( + "/required", + gooseRestRouter(RequiredModel, { + permissions: { + list: [Permissions.IsAny], + create: [Permissions.IsAuthenticated], + read: [Permissions.IsAny], + update: [Permissions.IsOwner], + delete: [Permissions.IsAdmin], + }, + }) + ); server = supertest(app); }); @@ -217,7 +425,7 @@ describe("mongoose rest framework", () => { name: "Broccoli", calories: 15, }); - assert.equal(res.status, 200); + assert.equal(res.status, 201); }); it("patch own item", async function() { @@ -255,6 +463,7 @@ describe("mongoose rest framework", () => { describe("admin food", function() { let agent: supertest.SuperAgentTest; let token: string; + beforeEach(async function() { agent = supertest.agent(app); const res = await agent @@ -284,7 +493,7 @@ describe("mongoose rest framework", () => { name: "Broccoli", calories: 15, }); - assert.equal(res.status, 200); + assert.equal(res.status, 201); }); it("patch", async function() { @@ -303,7 +512,17 @@ describe("mongoose rest framework", () => { const res2 = await agent .delete(`/food/${res.body.data[0]._id}`) .set("authorization", `Bearer ${token}`); - assert.equal(res2.status, 200); + assert.equal(res2.status, 204); + }); + + it("handles validation errors", async function() { + const res = await agent + .post("/required") + .set("authorization", `Bearer ${token}`) + .send({ + about: "Whoops forgot required", + }); + assert.equal(res.status, 400); }); }); }); @@ -313,16 +532,7 @@ describe("mongoose rest framework", () => { let admin: any; beforeEach(async function() { - await Promise.all([UserModel.deleteMany({}), FoodModel.deleteMany({})]); - [notAdmin, admin] = await Promise.all([ - UserModel.create({email: "notAdmin@example.com"}), - UserModel.create({email: "admin@example.com", admin: true}), - ]); - await (notAdmin as any).setPassword("password"); - await notAdmin.save(); - - await (admin as any).setPassword("securePassword"); - await admin.save(); + [admin, notAdmin] = await setupDb(); await Promise.all([ FoodModel.create({ @@ -590,16 +800,7 @@ describe("mongoose rest framework", () => { let admin: any; beforeEach(async function() { - await Promise.all([UserModel.deleteMany({}), FoodModel.deleteMany({})]); - [notAdmin, admin] = await Promise.all([ - UserModel.create({email: "notAdmin@example.com"}), - UserModel.create({email: "admin@example.com", admin: true}), - ]); - await (notAdmin as any).setPassword("password"); - await notAdmin.save(); - - await (admin as any).setPassword("securePassword"); - await admin.save(); + [admin, notAdmin] = await setupDb(); [spinach, apple, carrots, pizza] = await Promise.all([ FoodModel.create({ diff --git a/src/mongooseRestFramework.ts b/src/mongooseRestFramework.ts index 24fed87..e7bd503 100644 --- a/src/mongooseRestFramework.ts +++ b/src/mongooseRestFramework.ts @@ -4,10 +4,10 @@ import session from "express-session"; import jwt from "jsonwebtoken"; import mongoose, {Document, Model, ObjectId, Schema} from "mongoose"; import passport from "passport"; -import {Strategy as JwtStrategy} from "passport-jwt"; import {Strategy as AnonymousStrategy} from "passport-anonymous"; +import {Strategy as JwtStrategy} from "passport-jwt"; import {Strategy as LocalStrategy} from "passport-local"; -import {logger} from "./expressServer"; +import {logger} from "./logger"; export interface Env { NODE_ENV?: string; @@ -51,17 +51,25 @@ interface User { } export interface UserModel extends Model { - createStrategy(): any; - serializeUser(): any; - deserializeUser(): any; createAnonymousUser?: (id?: string) => Promise; isValidPassword: (password: string) => boolean; - // Allows additional setup during signup. This will be passed the rest of req.body from the signup // request. postCreate?: (body: any) => Promise; + + createStrategy(): any; + + serializeUser(): any; + + // Allows additional setup during signup. This will be passed the rest of req.body from the signup + + deserializeUser(): any; } -type PermissionMethod = (method: RESTMethod, user?: User, obj?: T) => boolean; +export type PermissionMethod = ( + method: RESTMethod, + user?: User, + obj?: T +) => boolean | Promise; interface RESTPermissions { create: PermissionMethod[]; @@ -74,7 +82,7 @@ interface RESTPermissions { interface GooseRESTOptions { permissions: RESTPermissions; queryFields?: string[]; - // return null to prevent the query from runnning + // return null to prevent the query from running queryFilter?: (user?: User) => Record | null; transformer?: GooseTransformer; sort?: string | {[key: string]: "ascending" | "descending"}; @@ -83,13 +91,19 @@ interface GooseRESTOptions { defaultLimit?: number; // defaults to 100 maxLimit?: number; // defaults to 500 endpoints?: (router: any) => void; + preCreate?: (value: any, request: express.Request) => T | null; + preUpdate?: (value: any, request: express.Request) => T | null; + preDelete?: (value: any, request: express.Request) => T | null; + postCreate?: (value: any, request: express.Request) => void | Promise; + postUpdate?: (value: any, request: express.Request) => void | Promise; + postDelete?: (request: express.Request) => void | Promise; } export const OwnerQueryFilter = (user?: User) => { if (user) { return {ownerId: user?.id}; } - // Return a null so we know to return no results. + // Return a null, so we know to return no results. return null; }; @@ -98,10 +112,7 @@ export const Permissions = { if (user?.id && !user?.isAnonymous) { return true; } - if (method === "list" || method === "read") { - return true; - } - return false; + return method === "list" || method === "read"; }, IsOwnerOrReadOnly: (method: RESTMethod, user?: User, obj?: any) => { // When checking if we can possibly perform the action, return true. @@ -115,10 +126,7 @@ export const Permissions = { if (user?.id && obj?.ownerId && String(obj?.ownerId) === String(user?.id)) { return true; } - if (method === "list" || method === "read") { - return true; - } - return false; + return method === "list" || method === "read"; }, IsAny: () => { return true; @@ -148,15 +156,16 @@ export const Permissions = { }; // Defaults closed -export function checkPermissions( +export async function checkPermissions( method: RESTMethod, permissions: PermissionMethod[], user?: User, obj?: T -): boolean { +): Promise { let anyTrue = false; for (const perm of permissions) { - if (perm(method, user, obj) === false) { + // May or may not be a promise. + if (!(await perm(method, user, obj))) { return false; } else { anyTrue = true; @@ -186,7 +195,7 @@ export function tokenPlugin(schema: Schema, options: {expiresIn?: number} = {}) } this.token = jwt.sign({id: this._id.toString()}, secretOrKey, tokenOptions); } - // On any save, update updated. + // On any save, update the updated field. this.updated = new Date(); next(); }); @@ -196,6 +205,7 @@ export interface BaseUser { admin: boolean; email: string; } + export function baseUserPlugin(schema: Schema) { schema.add({admin: {type: Boolean, default: false}}); schema.add({email: {type: String, index: true}}); @@ -233,7 +243,7 @@ export function createdDeletedPlugin(schema: Schema) { if (!this.created) { this.created = new Date(); } - // All writes update updated. + // All writes change the updated time. this.updated = new Date(); next(); }); @@ -278,7 +288,7 @@ export function setupAuth(app: express.Application, userModel: UserModel) { } await user.save(); if (!user.token) { - throw new Error("Token not created"); + return done(new Error("Token not created")); } return done(null, user); } catch (error) { @@ -429,6 +439,9 @@ export function setupAuth(app: express.Application, userModel: UserModel) { // } try { const data = await userModel.findOneAndUpdate({_id: req.user.id}, req.body, {new: true}); + if (data === null) { + return res.status(404).send(); + } const dataObject = data.toObject(); (dataObject as any).id = data._id; return res.json({data: dataObject}); @@ -569,7 +582,7 @@ export function gooseRestRouter( // TODO Toggle anonymous auth middleware based on settings for route. router.post("/", authenticateMiddleware(true), async (req, res) => { - if (!checkPermissions("create", options.permissions.create, req.user)) { + if (!(await checkPermissions("create", options.permissions.create, req.user))) { logger.warn(`Access to CREATE on ${model.name} denied for ${req.user?.id}`); return res.status(405).send(); } @@ -580,12 +593,35 @@ export function gooseRestRouter( } catch (e) { return res.status(403).send({message: (e as any).message}); } - const data = await model.create(body); - return res.json({data: serialize(data, req.user)}); + if (options.preCreate) { + try { + body = options.preCreate(body, req); + } catch (e) { + return res.status(400).send({message: `Pre Create error: ${(e as any).message}`}); + } + if (body === null) { + return res.status(403).send({message: "Pre Create returned null"}); + } + } + let data; + try { + data = await model.create(body); + } catch (e) { + return res.status(400).send({message: (e as any).message}); + } + if (options.postCreate) { + try { + await options.postCreate(data, req); + } catch (e) { + return res.status(400).send({message: `Post Create error: ${(e as any).message}`}); + } + } + return res.status(201).json({data: serialize(data, req.user)}); }); + // TODO add rate limit router.get("/", authenticateMiddleware(true), async (req, res) => { - if (!checkPermissions("list", options.permissions.list, req.user)) { + if (!(await checkPermissions("list", options.permissions.list, req.user))) { logger.warn(`Access to LIST on ${model.name} denied for ${req.user?.id}`); return res.status(403).send(); } @@ -658,7 +694,7 @@ export function gooseRestRouter( try { data = await builtQuery.exec(); } catch (e) { - logger.error("List error", e); + logger.error(`List error: ${(e as any).stack}`); return res.status(500).send(); } // TODO add pagination @@ -671,7 +707,7 @@ export function gooseRestRouter( }); router.get("/:id", authenticateMiddleware(true), async (req, res) => { - if (!checkPermissions("read", options.permissions.read, req.user)) { + if (!(await checkPermissions("read", options.permissions.read, req.user))) { logger.warn(`Access to READ on ${model.name} denied for ${req.user?.id}`); return res.status(405).send(); } @@ -682,7 +718,7 @@ export function gooseRestRouter( return res.status(404).send(); } - if (!checkPermissions("read", options.permissions.read, req.user, data)) { + if (!(await checkPermissions("read", options.permissions.read, req.user, data))) { logger.warn(`Access to READ on ${model.name}:${req.params.id} denied for ${req.user?.id}`); return res.status(403).send(); } @@ -696,7 +732,7 @@ export function gooseRestRouter( }); router.patch("/:id", authenticateMiddleware(true), async (req, res) => { - if (!checkPermissions("update", options.permissions.update, req.user)) { + if (!(await checkPermissions("update", options.permissions.update, req.user))) { logger.warn(`Access to PATCH on ${model.name} denied for ${req.user?.id}`); return res.status(405).send(); } @@ -707,7 +743,7 @@ export function gooseRestRouter( return res.status(404).send(); } - if (!checkPermissions("update", options.permissions.update, req.user, doc)) { + if (!(await checkPermissions("update", options.permissions.update, req.user, doc))) { logger.warn(`Patch not allowed for user ${req.user?.id} on doc ${doc._id}`); return res.status(403).send(); } @@ -719,12 +755,36 @@ export function gooseRestRouter( logger.warn(`Patch failed for user ${req.user?.id}: ${(e as any).message}`); return res.status(403).send({message: (e as any).message}); } - doc = await model.findOneAndUpdate({_id: req.params.id}, body, {new: true}); + + if (options.preUpdate) { + try { + body = options.preUpdate(body, req); + } catch (e) { + return res.status(400).send({message: `Pre Update error: ${(e as any).message}`}); + } + if (body === null) { + return res.status(403).send({message: "Pre Update returned null"}); + } + } + + try { + doc = await model.findOneAndUpdate({_id: req.params.id}, body as any, {new: true}); + } catch (e) { + return res.status(400).send({message: (e as any).message}); + } + + if (options.postUpdate) { + try { + await options.postUpdate(doc, req); + } catch (e) { + return res.status(400).send({message: `Post Update error: ${(e as any).message}`}); + } + } return res.json({data: serialize(doc, req.user)}); }); router.delete("/:id", authenticateMiddleware(true), async (req, res) => { - if (!checkPermissions("delete", options.permissions.delete, req.user)) { + if (!(await checkPermissions("delete", options.permissions.delete, req.user))) { logger.warn(`Access to DELETE on ${model.name} denied for ${req.user?.id}`); return res.status(405).send(); } @@ -735,12 +795,37 @@ export function gooseRestRouter( return res.status(404).send(); } - if (!checkPermissions("delete", options.permissions.delete, req.user, data)) { + if (!(await checkPermissions("delete", options.permissions.delete, req.user, data))) { logger.warn(`Access to DELETE on ${model.name}:${req.params.id} denied for ${req.user?.id}`); return res.status(403).send(); } - return res.json({data: serialize(data, req.user)}); + if (options.preDelete) { + try { + const body = options.preDelete(data, req); + if (body === null) { + return res.status(403).send({message: "Pre Delete returned null"}); + } + } catch (e) { + return res.status(400).send({message: `Pre Delete error: ${(e as any).message}`}); + } + } + + try { + await data.remove(); + } catch (e) { + return res.status(400).send({message: (e as any).message}); + } + + if (options.postDelete) { + try { + await options.postDelete(req); + } catch (e) { + return res.status(400).send({message: `Post Delete error: ${(e as any).message}`}); + } + } + + return res.status(204).send(); }); return router; diff --git a/src/passport.ts b/src/passport.ts index 8b48cc0..6e628ed 100644 --- a/src/passport.ts +++ b/src/passport.ts @@ -301,7 +301,7 @@ export const passportLocalMongoose = function(schema: Schema, opts: Partial cb(null, result)).catch((err) => cb(err)); }; - schema.methods.changePassword = function(oldPassword, newPassword, cb) { + schema.methods.changePassword = function(oldPassword: string, newPassword: string, cb?: any) { const promise = Promise.resolve() .then(() => { if (!oldPassword || !newPassword) { @@ -332,7 +332,7 @@ export const passportLocalMongoose = function(schema: Schema, opts: Partial { if (user) { diff --git a/yarn.lock b/yarn.lock index d8e849a..2971820 100644 --- a/yarn.lock +++ b/yarn.lock @@ -697,20 +697,6 @@ "@types/connect" "*" "@types/node" "*" -"@types/bson@*": - version "4.2.0" - resolved "https://registry.yarnpkg.com/@types/bson/-/bson-4.2.0.tgz#a2f71e933ff54b2c3bf267b67fa221e295a33337" - integrity sha512-ELCPqAdroMdcuxqwMgUpifQyRoTpyYCNr1V9xKyF40VsBobsj+BbWNRvwGchMgBPGqkw655ypkjj2MEF5ywVwg== - dependencies: - bson "*" - -"@types/bson@1.x || 4.0.x": - version "4.0.5" - resolved "https://registry.yarnpkg.com/@types/bson/-/bson-4.0.5.tgz#9e0e1d1a6f8866483f96868a9b33bc804926b1fc" - integrity sha512-vVLwMUqhYJSQ/WKcE60eFqcyuWse5fGH+NMAXHuKrUAPoryq3ATxk5o4bgYNtg5aOM4APVg7Hnb3ASqUYG0PKg== - dependencies: - "@types/node" "*" - "@types/chai@^4.2.22": version "4.2.22" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.22.tgz#47020d7e4cf19194d43b5202f35f75bd2ad35ce7" @@ -823,18 +809,10 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== -"@types/mongodb@^3.5.27": - version "3.6.20" - resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.6.20.tgz#b7c5c580644f6364002b649af1c06c3c0454e1d2" - integrity sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ== - dependencies: - "@types/bson" "*" - "@types/node" "*" - "@types/node@*": - version "16.10.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.10.3.tgz#7a8f2838603ea314d1d22bb3171d899e15c57bd5" - integrity sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ== + version "17.0.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.8.tgz#50d680c8a8a78fe30abe6906453b21ad8ab0ad7b" + integrity sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg== "@types/node@^14.14.27": version "14.17.21" @@ -869,15 +847,7 @@ "@types/jsonwebtoken" "*" "@types/passport-strategy" "*" -"@types/passport-local-mongoose@^6.1.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@types/passport-local-mongoose/-/passport-local-mongoose-6.1.0.tgz#67c550c46dfd480c607dad1836d74738225855df" - integrity sha512-5pIlcruo48uzBKv3LlNM9bolS11TOy5TnZytFH4UC4EEjZCZcN0r8fBB+JcQqGCXxq+Uk+UDLDdXcsqkntp01A== - dependencies: - "@types/passport-local" "*" - mongoose "5.*" - -"@types/passport-local@*": +"@types/passport-local@^1.0.34": version "1.0.34" resolved "https://registry.yarnpkg.com/@types/passport-local/-/passport-local-1.0.34.tgz#84d3b35b2fd4d36295039ded17fe5f3eaa62f4f6" integrity sha512-PSc07UdYx+jhadySxxIYWuv6sAnY5e+gesn/5lkPKfBeGuIYn9OPR+AAEDq73VRUh6NBTpvE/iPE62rzZUslog== @@ -1438,19 +1408,6 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== -bl@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/bl/-/bl-2.2.1.tgz#8c11a7b730655c5d56898cdc871224f40fd901d5" - integrity sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g== - dependencies: - readable-stream "^2.3.5" - safe-buffer "^5.1.1" - -bluebird@3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" - integrity sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA== - body-parser@1.19.0: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" @@ -1528,18 +1485,13 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" -bson@*, bson@^4.2.2, bson@^4.5.2: - version "4.5.3" - resolved "https://registry.yarnpkg.com/bson/-/bson-4.5.3.tgz#de3783b357a407d935510beb1fbb285fef43bb06" - integrity sha512-qVX7LX79Mtj7B3NPLzCfBiCP6RAsjiV8N63DjlaVVpZW+PFoDTxQ4SeDbSpcqgE6mXksM5CAwZnXxxxn/XwC0g== +bson@^4.2.2, bson@^4.6.1: + version "4.6.1" + resolved "https://registry.yarnpkg.com/bson/-/bson-4.6.1.tgz#2b5da517539bb0f7f3ffb54ac70a384ca899641c" + integrity sha512-I1LQ7Hz5zgwR4QquilLNZwbhPw0Apx7i7X9kGMBTsqPdml/03Q9NBtD9nt/19ahjlphktQImrnderxqpzeVDjw== dependencies: buffer "^5.6.0" -bson@^1.1.4: - version "1.1.6" - resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.6.tgz#fb819be9a60cd677e0853aee4ca712a785d6618a" - integrity sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg== - 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" @@ -1845,11 +1797,6 @@ core-js@^3.16.2: resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.18.2.tgz#63a551e8a29f305cd4123754846e65896619ba5b" integrity sha512-zNhPOUoSgoizoSQFdX1MeZO16ORRb9FFQLts8gSYbZU5FcgXhp24iMWMxnOQo5uIaIG7/6FA/IqJPwev1o9ZXQ== -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== - cron@^1.8.2: version "1.8.2" resolved "https://registry.yarnpkg.com/cron/-/cron-1.8.2.tgz#4ac5e3c55ba8c163d84f3407bde94632da8370ce" @@ -1915,20 +1862,20 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: dependencies: ms "2.0.0" -debug@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - -debug@4, debug@4.x, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: +debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: version "4.3.2" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== dependencies: ms "2.1.2" +debug@4.x: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -2002,11 +1949,6 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -denque@^1.4.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" - integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== - denque@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/denque/-/denque-2.0.1.tgz#bcef4c1b80dc32efe97515744f21a4229ab8934a" @@ -2568,6 +2510,14 @@ expect@^26.6.2: jest-message-util "^26.6.2" jest-regex-util "^26.0.0" +expo-server-sdk@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/expo-server-sdk/-/expo-server-sdk-3.6.0.tgz#b13e5e77f622d11009bdd533df857b08225a4d00" + integrity sha512-GyA0BTcFBKk/5gTEO4WOScP9hEttR+GitrcOIl7XwXwE1FHFvbluKiUc9yEjsfEYMgyd78+XhSpGVGQnutGOdA== + dependencies: + node-fetch "^2.6.0" + promise-limit "^2.7.0" + express-session@^1.17.2: version "1.17.2" resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.17.2.tgz#397020374f9bf7997f891b85ea338767b30d0efd" @@ -3173,7 +3123,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.3, 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== @@ -3211,6 +3161,11 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +ip@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -3464,7 +3419,7 @@ isarray@0.0.1: resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= -isarray@1.0.0, isarray@~1.0.0: +isarray@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= @@ -4038,10 +3993,10 @@ jws@^3.2.2: jwa "^1.4.1" safe-buffer "^5.0.1" -kareem@2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.2.tgz#78c4508894985b8d38a0dc15e1a8e11078f2ca93" - integrity sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ== +kareem@2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.3.tgz#a4432d7965a5bb06fc2b4eeae71317344c9a756a" + integrity sha512-uESCXM2KdtOQ8LOvKyTUXEeg0MkYp4wGglTIpGcYHvjJcS5sn2Wkfrfit8m4xSbaNDAw2KdI9elgkOxZbrFYbg== kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" @@ -4407,102 +4362,50 @@ moment-timezone@^0.5.x: resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== -mongodb-connection-string-url@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mongodb-connection-string-url/-/mongodb-connection-string-url-2.1.0.tgz#9c522c11c37f571fecddcb267ac4a76ef432aeb7" - integrity sha512-Qf9Zw7KGiRljWvMrrUFDdVqo46KIEiDuCzvEN97rh/PcKzk2bd6n9KuzEwBwW9xo5glwx69y1mI6s+jFUD/aIQ== +mongodb-connection-string-url@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/mongodb-connection-string-url/-/mongodb-connection-string-url-2.4.2.tgz#422861119764796420889f9b0416b957e1b0bde0" + integrity sha512-mZUXF6nUzRWk5J3h41MsPv13ukWlH4jOMSk6astVeoZ1EbdTJyF5I3wxKkvqBAOoVtzLgyEYUvDjrGdcPlKjAw== dependencies: "@types/whatwg-url" "^8.2.1" - whatwg-url "^9.1.0" - -mongodb@3.6.11: - version "3.6.11" - resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.6.11.tgz#8a59a0491a92b00a8c925f72ed9d9a5b054aebb2" - integrity sha512-4Y4lTFHDHZZdgMaHmojtNAlqkvddX2QQBEN0K//GzxhGwlI9tZ9R0vhbjr1Decw+TF7qK0ZLjQT292XgHRRQgw== - dependencies: - bl "^2.2.1" - bson "^1.1.4" - denque "^1.4.1" - optional-require "^1.0.3" - safe-buffer "^5.1.2" - optionalDependencies: - saslprep "^1.0.0" + whatwg-url "^11.0.0" -mongodb@4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-4.1.2.tgz#36ab494db3a9a827df41ccb0d9b36a94bfeae8d7" - integrity sha512-pHCKDoOy1h6mVurziJmXmTMPatYWOx8pbnyFgSgshja9Y36Q+caHUzTDY6rrIy9HCSrjnbXmx3pCtvNZHmR8xg== +mongodb@4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-4.3.1.tgz#e346f76e421ec6f47ddea5c8f5140e6181aaeb94" + integrity sha512-sNa8APSIk+r4x31ZwctKjuPSaeKuvUeNb/fu/3B6dRM02HpEgig7hTHM8A/PJQTlxuC/KFWlDlQjhsk/S43tBg== dependencies: - bson "^4.5.2" + bson "^4.6.1" denque "^2.0.1" - mongodb-connection-string-url "^2.0.0" + mongodb-connection-string-url "^2.4.1" + socks "^2.6.1" optionalDependencies: saslprep "^1.0.3" -mongoose-legacy-pluralize@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4" - integrity sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ== - -mongoose@5.*: - version "5.13.10" - resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.13.10.tgz#42d66245a084902cc7ee7fb932cb87f09d2a94fe" - integrity sha512-h1tkPu/3grQjdWjoIA3uUNWvDFDcaNGwcmBLEkK9t/kzKq4L4fB2EoA/VLjQ7WGTL/pDKH6OElBTK7x/QPRvbg== - dependencies: - "@types/bson" "1.x || 4.0.x" - "@types/mongodb" "^3.5.27" - bson "^1.1.4" - kareem "2.3.2" - mongodb "3.6.11" - mongoose-legacy-pluralize "1.0.2" - mpath "0.8.4" - mquery "3.2.5" - ms "2.1.2" - optional-require "1.0.x" - regexp-clone "1.0.0" - safe-buffer "5.2.1" - sift "13.5.2" - sliced "1.0.1" - -mongoose@^6.0.8: - version "6.0.9" - resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-6.0.9.tgz#833a300f515dc17fb88850f5f89753ec69623411" - integrity sha512-j9wcL8sltyIPBzMv785HFuGOdO8a5b70HX+e1q5QOogJxFofEXQoCcuurGlFSOe6j8M25qxHLzeVeKVcITeviQ== +mongoose@^6.1.9: + version "6.2.1" + resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-6.2.1.tgz#5791f46336f785080259c007ec16ad42e606e2ee" + integrity sha512-VxY1wvlc4uBQKyKNVDoEkTU3/ayFOD//qVXYP+sFyvTRbAj9/M53UWTERd84pWogs2TqAC6DTvZbxCs2LoOd3Q== dependencies: bson "^4.2.2" - kareem "2.3.2" - mongodb "4.1.2" + kareem "2.3.3" + mongodb "4.3.1" mpath "0.8.4" - mquery "4.0.0" + mquery "4.0.2" ms "2.1.2" - regexp-clone "1.0.0" sift "13.5.2" - sliced "1.0.1" mpath@0.8.4: version "0.8.4" resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.8.4.tgz#6b566d9581621d9e931dd3b142ed3618e7599313" integrity sha512-DTxNZomBcTWlrMW76jy1wvV37X/cNNxPW1y2Jzd4DZkAaC5ZGsm8bfGfNOthcDuRJujXLqiuS6o3Tpy0JEoh7g== -mquery@3.2.5: - version "3.2.5" - resolved "https://registry.yarnpkg.com/mquery/-/mquery-3.2.5.tgz#8f2305632e4bb197f68f60c0cffa21aaf4060c51" - integrity sha512-VjOKHHgU84wij7IUoZzFRU07IAxd5kWJaDmyUzQlbjHjyoeK5TNeeo8ZsFDtTYnSgpW6n/nMNIHvE3u8Lbrf4A== - dependencies: - bluebird "3.5.1" - debug "3.1.0" - regexp-clone "^1.0.0" - safe-buffer "5.1.2" - sliced "1.0.1" - -mquery@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/mquery/-/mquery-4.0.0.tgz#6c62160ad25289e99e0840907757cdfd62bde775" - integrity sha512-nGjm89lHja+T/b8cybAby6H0YgA4qYC/lx6UlwvHGqvTq8bDaNeCwl1sY8uRELrNbVWJzIihxVd+vphGGn1vBw== +mquery@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/mquery/-/mquery-4.0.2.tgz#a13add5ecd7c2e5a67e0f814b3c7acdfb6772804" + integrity sha512-oAVF0Nil1mT3rxty6Zln4YiD6x6QsUWYz927jZzjMxOK2aqmhEz5JQ7xmrKK7xRFA2dwV+YaOpKU/S+vfNqKxA== dependencies: debug "4.x" - regexp-clone "^1.0.0" - sliced "1.0.1" ms@2.0.0: version "2.0.0" @@ -4572,6 +4475,13 @@ nise@^1.5.2: lolex "^5.0.1" path-to-regexp "^1.7.0" +node-fetch@^2.6.0: + version "2.6.6" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89" + integrity sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA== + dependencies: + whatwg-url "^5.0.0" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -4768,18 +4678,6 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" -optional-require@1.0.x: - version "1.0.3" - resolved "https://registry.yarnpkg.com/optional-require/-/optional-require-1.0.3.tgz#275b8e9df1dc6a17ad155369c2422a440f89cb07" - integrity sha512-RV2Zp2MY2aeYK5G+B/Sps8lW5NHAzE5QClbFP15j+PWmP+T9PxlJXBOOLoSAdgwFvS4t0aMR4vpedMkbHfh0nA== - -optional-require@^1.0.3: - version "1.1.8" - resolved "https://registry.yarnpkg.com/optional-require/-/optional-require-1.1.8.tgz#16364d76261b75d964c482b2406cb824d8ec44b7" - integrity sha512-jq83qaUb0wNg9Krv1c5OQ+58EK+vHde6aBPzLvPPqJm89UQWsvSuFy9X/OSNJnFeSOKo7btE0n8Nl2+nE+z5nA== - dependencies: - require-at "^1.0.6" - optionator@^0.8.1, optionator@^0.8.2: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -5104,16 +5002,16 @@ pretty-format@^26.0.0, pretty-format@^26.6.2: ansi-styles "^4.0.0" react-is "^17.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== - progress@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +promise-limit@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/promise-limit/-/promise-limit-2.7.0.tgz#eb5737c33342a030eaeaecea9b3d3a93cb592b26" + integrity sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw== + prompts@^2.0.1: version "2.4.1" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.1.tgz#befd3b1195ba052f9fd2fde8a486c4e82ee77f61" @@ -5245,19 +5143,6 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -readable-stream@^2.3.5: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - 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.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" @@ -5280,11 +5165,6 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexp-clone@1.0.0, regexp-clone@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-1.0.0.tgz#222db967623277056260b992626354a04ce9bf63" - integrity sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw== - regexp.prototype.flags@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26" @@ -5318,11 +5198,6 @@ repeat-string@^1.6.1: resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= -require-at@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/require-at/-/require-at-1.0.6.tgz#9eb7e3c5e00727f5a4744070a7f560d4de4f6e6a" - integrity sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g== - require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -5427,12 +5302,12 @@ rxjs@^6.4.0: dependencies: tslib "^1.9.0" -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.1.2, 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.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -5469,7 +5344,7 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -saslprep@^1.0.0, saslprep@^1.0.3: +saslprep@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226" integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag== @@ -5650,10 +5525,10 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" -sliced@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" - integrity sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E= +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== snapdragon-node@^2.0.1: version "2.1.1" @@ -5685,6 +5560,14 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" +socks@^2.6.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.2.tgz#ec042d7960073d40d94268ff3bb727dc685f111a" + integrity sha512-zDZhHhZRY9PxRruRMR7kMhnf3I8hDs4S3f9RecfnGxvcBHQcKcIH/oUcEWffsfl1XxdYlA7nnlGbbTvPz9D8gA== + dependencies: + ip "^1.1.5" + smart-buffer "^4.2.0" + source-map-resolve@^0.5.0: version "0.5.3" resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" @@ -5865,13 +5748,6 @@ string_decoder@^1.1.1: 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@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" @@ -6087,6 +5963,18 @@ tr46@^2.1.0: dependencies: punycode "^2.1.1" +tr46@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" + integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== + dependencies: + punycode "^2.1.1" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + triple-beam@^1.2.0, triple-beam@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" @@ -6239,7 +6127,7 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== -util-deprecate@^1.0.1, util-deprecate@~1.0.1: +util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= @@ -6297,6 +6185,11 @@ walker@^1.0.7, walker@~1.0.5: dependencies: makeerror "1.0.x" +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + webidl-conversions@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" @@ -6307,6 +6200,11 @@ webidl-conversions@^6.1.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + whatwg-encoding@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" @@ -6319,6 +6217,22 @@ whatwg-mimetype@^2.3.0: resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== +whatwg-url@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018" + integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ== + dependencies: + tr46 "^3.0.0" + webidl-conversions "^7.0.0" + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + whatwg-url@^8.0.0, whatwg-url@^8.5.0: version "8.7.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77" @@ -6328,14 +6242,6 @@ whatwg-url@^8.0.0, whatwg-url@^8.5.0: tr46 "^2.1.0" webidl-conversions "^6.1.0" -whatwg-url@^9.1.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-9.1.0.tgz#1b112cf237d72cd64fa7882b9c3f6234a1c3050d" - integrity sha512-CQ0UcrPHyomtlOCot1TL77WyMIm/bCwrJ2D6AOKGwEczU9EpyoqAokfqrf/MioU9kHcMsmJZcg1egXix2KYEsA== - dependencies: - tr46 "^2.1.0" - webidl-conversions "^6.1.0" - which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"