Vital App - Auto reschedule based on health data (#2500)

* Add vital integration

* Tidy up client_user_id creation

* Rename vital app to vitalother to follow name rules

* Added env var

* App vital reschedule

* Fix on app structure and api calls

* Implemented user identification from webhook

* WIP fix api call and read me

* Save vital settings via api

* Now saving userVitalSettings and trigger reschedule on selected param

* Added translations

* Fix type for vitalSettings

* Using api to get env vars required for url, fix display of vital settings

* Fix hours placeholder, translation not working

* Renames vital app

* Update seed-app-store.ts

* Update package.json

* Update yarn.lock

* Refactored env variables

* Update README.md

* Migrates to api_keys

* Extracts AppConfiguration

* vitalClient fixes

* Update index.ts

* Update metadata.ts

* Update index.ts

* Update metadata.ts

* Added namespace vital for translations

Co-authored-by: Maitham <maithamdib@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
alannnc 2022-05-06 12:21:30 -05:00 committed by GitHub
parent 67cc3a6409
commit 8c9096b55b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 2650 additions and 35 deletions

View File

@ -71,4 +71,14 @@ ZOOM_CLIENT_SECRET=
# Used for the Giphy integration
# @see https://support.giphy.com/hc/en-us/articles/360020283431-Request-A-GIPHY-API-Key
GIPHY_API_KEY=
# - VITAL
# Used for the vital integration
# @see https://github.com/calcom/cal.com/#obtaining-vital-api-keys
VITAL_API_KEY=
VITAL_WEBHOOK_SECRET=
# "sandbox" | "prod" | "production" | "development"
VITAL_DEVELOPMENT_MODE="sandbox"
# "us" | "eu"
VITAL_REGION="us"
# *********************************************************************************************************

View File

@ -107,7 +107,7 @@ Here is what you need to be able to run Cal.
```sh
yarn
```
1. Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the .env file.
#### Quick start with `yarn dx`
@ -232,9 +232,9 @@ yarn workspace @calcom/web playwright-report
1. Check for `.env` variables changes
```sh
yarn predev
```
```sh
yarn predev
```
1. Start the server. In a development environment, just do:
@ -403,6 +403,17 @@ Next make sure you have your app running `yarn dx`. Then in the slack chat type
9. Click the "Save" button at the bottom footer.
10. You're good to go. Now you can see any booking in Cal.com created as a meeting in HubSpot for your contacts.
### Obtaining Vital API Keys
1. Open [Vital](https://tryvital.io/) and click Get API Keys.
1. Create a team with the team name you desire
1. Head to the configuration section on the sidebar of the dashboard
1. Click on API keys and you'll find your sandbox `api_key`.
1. Copy your `api_key` to `VITAL_API_KEY` in the .env.appStore file.
1. Open [Vital Webhooks](https://app.tryvital.io/team/{team_id}/webhooks) and add `<CALCOM BASE URL>/api/integrations/vital/webhook` as webhook for connected applications.
1. Select all events for the webhook you interested, e.g. `sleep_created`
1. Copy the webhook secret (`sec...`) to `VITAL_WEBHOOK_SECRET` in the .env.appStore file.
<!-- LICENSE -->
## License

View File

@ -1,6 +1,7 @@
const path = require("path");
module.exports = {
/** @type {import("next-i18next").UserConfig} */
const config = {
i18n: {
defaultLocale: "en",
locales: [
@ -31,3 +32,5 @@ module.exports = {
localePath: path.resolve("./public/static/locales"),
reloadOnPrerender: process.env.NODE_ENV !== "production",
};
module.exports = config;

View File

@ -3,7 +3,7 @@ import Image from "next/image";
import React, { useEffect, useState } from "react";
import { JSONObject } from "superjson/dist/types";
import { InstallAppButton } from "@calcom/app-store/components";
import { AppConfiguration, InstallAppButton } from "@calcom/app-store/components";
import showToast from "@calcom/lib/notification";
import { App } from "@calcom/types/App";
import { Alert } from "@calcom/ui/Alert";
@ -161,8 +161,9 @@ function IntegrationsContainer() {
isGlobal={item.isGlobal}
installed={item.installed}
/>
}
/>
}>
<AppConfiguration type={item.type} credentialIds={item.credentialIds} />
</IntegrationListItem>
))}
</List>
</>

View File

@ -0,0 +1,13 @@
{
"connected_vital_app": "Connected with",
"vital_app_sleep_automation": "Sleeping reschedule automation",
"vital_app_automation_description": "You can select different parameters to trigger the reschedule based on your sleeping metrics.",
"vital_app_parameter": "Parameter",
"vital_app_trigger": "Trigger at below or equal than",
"vital_app_save_button": "Save configuration",
"vital_app_total_label": "Total (total = rem + light sleep + deep sleep)",
"vital_app_duration_label": "Duration (duration = bedtime end - bedtime start)",
"vital_app_hours": "hours",
"vital_app_save_success": "Success saving your Vital Configurations",
"vital_app_save_error": "An error ocurred saving your Vital Configurations"
}

View File

@ -0,0 +1,13 @@
{
"connected_vital_app": "Conectado con",
"vital_app_sleep_automation": "Automatización de reagendado en base al patron de sueño",
"vital_app_automation_description": "Puedes seleccionar diferentes parámetros para activar el reagendado automático en base a tus patrones de sueño.",
"vital_app_parameter": "Parámetro",
"vital_app_trigger": "Activar cuando sea igual o menor que",
"vital_app_save_button": "Guardar configuración",
"vital_app_total_label": "Total (total = rem + sueño ligero + sueño profundo)",
"vital_app_duration_label": "Duration (duration = Hora que te levantaste de la cama - Hora que te acostaste en la cama)",
"vital_app_hours": "horas",
"vital_app_save_success": "Fue un éxito el guardado de tus configuraciones de App Vital",
"vital_app_save_error": "Ocurrió un error al intentar guardar tus configuraciones de App Vital"
}

View File

@ -105,7 +105,7 @@ export const createContext = async ({ req }: CreateContextOptions) => {
const user = await getUserFromSession({ session, req });
const locale = user?.locale ?? getLocaleFromHeaders(req);
const i18n = await serverSideTranslations(locale, ["common"]);
const i18n = await serverSideTranslations(locale, ["common", "vital"]);
return {
i18n,
prisma,

View File

@ -0,0 +1,19 @@
import dynamic from "next/dynamic";
export const ConfigAppMap = {
vital: dynamic(() => import("../vital/components/AppConfiguration")),
};
export const AppConfiguration = (props: { type: string } & { credentialIds: number[] }) => {
let appName = props.type.replace(/_/g, "");
let ConfigAppComponent = ConfigAppMap[appName as keyof typeof ConfigAppMap];
/** So we can either call it by simple name (ex. `slack`, `giphy`) instead of
* `slackmessaging`, `giphyother` while maintaining retro-compatibility. */
if (!ConfigAppComponent) {
[appName] = props.type.split("_");
ConfigAppComponent = ConfigAppMap[appName as keyof typeof ConfigAppMap];
}
if (!ConfigAppComponent) return null;
return <ConfigAppComponent credentialIds={props.credentialIds} />;
};

View File

@ -8,6 +8,7 @@ export const apiHandlers = {
slackmessaging: import("./slackmessaging/api"),
stripepayment: import("./stripepayment/api"),
tandemvideo: import("./tandemvideo/api"),
vital: import("./vital/api"),
zoomvideo: import("@calcom/zoomvideo/api"),
office365video: import("@calcom/office365video/api"),
wipemycalother: import("./wipemycalother/api"),

View File

@ -27,6 +27,7 @@ export const InstallAppButtonMap = {
metamask: dynamic(() => import("./metamask/components/InstallAppButton")),
giphy: dynamic(() => import("./giphy/components/InstallAppButton")),
spacebookingother: dynamic(() => import("./spacebooking/components/InstallAppButton")),
vital: dynamic(() => import("./vital/components/InstallAppButton")),
};
export const InstallAppButton = (
@ -61,3 +62,5 @@ export const InstallAppButton = (
);
return <InstallAppButtonComponent render={props.render} onChanged={props.onChanged} />;
};
export { AppConfiguration } from "./_components/AppConfiguration";

View File

@ -15,6 +15,7 @@ import * as slackmessaging from "./slackmessaging";
import * as spacebooking from "./spacebooking";
import * as stripepayment from "./stripepayment";
import * as tandemvideo from "./tandemvideo";
import * as vital from "./vital";
import * as wipemycalother from "./wipemycalother";
import * as zapier from "./zapier";
import * as zoomvideo from "./zoomvideo";
@ -35,6 +36,7 @@ const appStore = {
stripepayment,
spacebooking,
tandemvideo,
vital,
zoomvideo,
wipemycalother,
metamask,

View File

@ -14,6 +14,7 @@ import { metadata as slackmessaging } from "./slackmessaging/_metadata";
import { metadata as spacebooking } from "./spacebooking/_metadata";
import { metadata as stripepayment } from "./stripepayment/_metadata";
import { metadata as tandemvideo } from "./tandemvideo/_metadata";
import { metadata as vital } from "./vital/_metadata";
import { metadata as wipemycalother } from "./wipemycalother/_metadata";
import { metadata as zapier } from "./zapier/_metadata";
import { metadata as zoomvideo } from "./zoomvideo/_metadata";
@ -33,6 +34,7 @@ export const appStoreMetadata = {
stripepayment,
spacebooking,
tandemvideo,
vital,
zoomvideo,
wipemycalother,
metamask,

View File

@ -0,0 +1,6 @@
Vital App is an app that can can help you combine your health peripherals with your calendar.
#### Supported Actions:
Sleep reschedule automation: Had a hard night? 🌕
Automatically reschedule your whole day schedule based on your sleep parameters. (Setup your desired configuration on installed apps page.)

View File

@ -0,0 +1,27 @@
import type { App } from "@calcom/types/App";
import _package from "./package.json";
export const metadata = {
name: "Vital",
description: _package.description,
installed: true,
category: "other",
// If using static next public folder, can then be referenced from the base URL (/).
imageSrc: "/api/app-store/vital/icon.svg",
logo: "/api/app-store/vital/icon.svg",
label: "Vital",
publisher: "Vital",
rating: 5,
reviews: 69,
slug: "vital-automation",
title: "Vital",
trending: true,
type: "vital_other",
url: "https://tryvital.io",
variant: "other",
verified: true,
email: "support@tryvital.io",
} as App;
export default metadata;

View File

@ -0,0 +1,41 @@
import { Prisma } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
/**
* This is will generate a user token for a client_user_id`
* @param req
* @param res
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const userWithMetadata = await prisma.user.findFirst({
where: {
id: req?.session?.user.id,
},
select: {
id: true,
metadata: true,
},
});
await prisma.user.update({
where: {
id: req?.session?.user.id,
},
data: {
metadata: {
...(userWithMetadata?.metadata as Prisma.JsonObject),
vitalSettings: {
...((userWithMetadata?.metadata as Prisma.JsonObject)?.vitalSettings as Prisma.JsonObject),
connected: true,
},
},
},
});
return res.redirect("/apps/installed");
} catch (e) {
return res.status(500);
}
}

View File

@ -0,0 +1,5 @@
export { default as token } from "./token";
export { default as callback } from "./callback";
export { default as webhook } from "./webhook";
export { default as settings } from "./settings";
export { default as save } from "./save";

View File

@ -0,0 +1,91 @@
import { Prisma } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { z, ZodError } from "zod";
import prisma from "@calcom/prisma";
export type VitalSettingsResponse = {
connected: boolean;
sleepValue: number;
selectedParam: string;
};
const vitalSettingsUpdateSchema = z.object({
connected: z.boolean().optional(),
selectedParam: z.string().optional(),
sleepValue: z.number().optional(),
});
const handler = async (
req: NextApiRequest,
res: NextApiResponse
): Promise<VitalSettingsResponse | NextApiResponse | void> => {
if (req.method === "PUT" && req.session && req.session.user.id) {
const userId = req.session.user.id;
const body = req.body;
try {
const userWithMetadata = await prisma.user.findFirst({
where: {
id: userId,
},
select: {
id: true,
metadata: true,
},
});
const userMetadata = userWithMetadata?.metadata as Prisma.JsonObject;
const vitalSettings =
((userWithMetadata?.metadata as Prisma.JsonObject)?.vitalSettings as Prisma.JsonObject) || {};
await prisma.user.update({
where: {
id: userId,
},
data: {
metadata: {
...userMetadata,
vitalSettings: {
...vitalSettings,
...body,
},
},
},
});
if (vitalSettings) {
res.status(200).json(vitalSettings);
} else {
res.status(404);
}
} catch (error) {
res.status(500);
}
} else {
res.status(400);
}
res.end();
};
function validate(
handler: (
req: NextApiRequest,
res: NextApiResponse
) => Promise<VitalSettingsResponse | NextApiResponse | void>
) {
return async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === "POST" || req.method === "PUT") {
try {
vitalSettingsUpdateSchema.parse(req.body);
} catch (error) {
if (error instanceof ZodError && error?.name === "ZodError") {
return res.status(400).json(error?.issues);
}
return res.status(402);
}
} else {
return res.status(405);
}
await handler(req, res);
};
}
export default validate(handler);

View File

@ -0,0 +1,29 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { JSONObject } from "superjson/dist/types";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET" && req.session && req.session.user.id) {
const userId = req.session.user.id;
try {
const user = await prisma?.user.findFirst({
select: {
metadata: true,
},
where: {
id: userId,
},
});
if (user && user.metadata && (user.metadata as JSONObject)?.vitalSettings) {
res.status(200).json((user.metadata as JSONObject).vitalSettings);
} else {
res.status(404);
}
} catch (error) {
res.status(500);
}
} else {
res.status(400);
}
res.end();
}

View File

@ -0,0 +1,23 @@
https://docs.tryvital.io/summary-data#sleep-stream
{
event: {
event_type: 'daily.data.sleep.created',
data: {
id: 'a27b89dc-4a43-4f59-b72a-267fa9a93c8c',
date: '1974-12-15T18:28:10+00:00',
bedtime_start: '1980-07-11T22:10:32+00:00',
bedtime_stop: '1993-03-03T04:21:02+00:00',
duration: 7225, // seconds
total: 6909, // seconds
awake: 7763,
light: 2011, // Total amount of light sleep registered during the sleep period
rem: 8106, // Total amount of REM sleep registered during the sleep period, minutes
deep: 6553,
hr_lowest: 9085,
efficiency: 1780,
latency: 8519,
source: [Object],
user_id: 'a1dfefe0-ccdb-46b8-adac-35e9ba597496'
}
}
}

View File

@ -0,0 +1,57 @@
import { Prisma } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { WEBAPP_URL } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
import { initVitalClient, vitalEnv } from "../lib/client";
/**
* This is will generate a user token for a client_user_id`
* @param req
* @param res
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// Get user id
const calcomUserId = req.session?.user?.id;
if (!calcomUserId) {
return res.status(401).json({ message: "You must be logged in to do this" });
}
const vitalClient = await initVitalClient();
if (!vitalClient || !vitalEnv)
return res.status(400).json({ message: "Missing vital client, try calling `initVitalClient`" });
// Create a user on vital
let userVital;
try {
userVital = await vitalClient.User.create(`cal_${calcomUserId}`);
} catch (e) {
userVital = await vitalClient.User.resolve(`cal_${calcomUserId}`);
}
try {
if (userVital?.user_id) {
await prisma.credential.create({
data: {
type: "vital_other",
key: { userVitalId: userVital.user_id } as unknown as Prisma.InputJsonObject,
userId: calcomUserId,
appId: "vital-automation",
},
});
}
const token = await vitalClient.Link.create(
userVital?.user_id,
undefined,
WEBAPP_URL + "/api/integrations/vital/callback"
);
return res.status(200).json({
token: token.link_token,
url: `https://link.tryvital.io/?env=${vitalEnv.mode}&region=${vitalEnv.region}`,
});
} catch (e) {
return res.status(400).json({ error: JSON.stringify(e) });
}
}

View File

@ -0,0 +1,158 @@
import { BookingStatus, Prisma } from "@prisma/client";
import dayjs from "dayjs";
import type { NextApiRequest, NextApiResponse } from "next";
import queue from "queue";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { HttpError as HttpCode } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import { Reschedule } from "../lib";
import { initVitalClient, vitalEnv } from "../lib/client";
// @Note: not being used anymore but left as example
const getOuraSleepScore = async (user_id: string, bedtime_start: Date) => {
const vitalClient = await initVitalClient();
if (!vitalClient) throw Error("Missing vital client");
const sleep_data = await vitalClient.Sleep.get_raw(user_id, bedtime_start, undefined, "oura");
if (sleep_data.sleep.length === 0) {
throw Error("No sleep score found");
}
return +sleep_data.sleep[0].data.score;
};
/**
* This is will generate a user token for a client_user_id`
* @param req
* @param res
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
if (req.method !== "POST") {
throw new HttpCode({ statusCode: 405, message: "Method Not Allowed" });
}
const sig = req.headers["svix-signature"];
if (!sig) {
throw new HttpCode({ statusCode: 400, message: "Missing svix-signature" });
}
const vitalClient = await initVitalClient();
if (!vitalClient || !vitalEnv)
return res.status(400).json({ message: "Missing vital client, try calling `initVitalClient`" });
const payload = JSON.stringify(req.body);
const event: any = vitalClient.Webhooks.constructWebhookEvent(
payload,
req.headers as Record<string, string>,
vitalEnv.webhook_secret as string
);
if (event.event_type == "daily.data.sleep.created") {
// Carry out logic here to determine what to do if sleep is less
// than 8 hours or readiness score is less than 70
try {
if (event.data.user_id) {
const json = { userVitalId: event.data.user_id as string };
const credential = await prisma.credential.findFirst({
rejectOnNotFound: true,
where: {
type: "vital_other",
key: {
equals: json,
},
},
});
if (!credential) {
return res.status(404).json({ message: "Missing vital credential" });
}
// Getting total hours of sleep seconds/60/60 = hours
const userWithMetadata = await prisma.user.findFirst({
select: {
metadata: true,
},
where: {
id: credential.userId as number,
},
});
let minimumSleepTime = 0;
let parameterFilter = "";
const userMetadata = userWithMetadata?.metadata as Prisma.JsonObject;
const vitalSettings =
((userWithMetadata?.metadata as Prisma.JsonObject)?.vitalSettings as Prisma.JsonObject) || {};
if (!!userMetadata && !!vitalSettings) {
minimumSleepTime = vitalSettings.sleepValue as number;
parameterFilter = vitalSettings.parameter as string;
} else {
res.status(404).json({ message: "Vital configuration not found for user" });
return;
}
if (!event.data.hasOwnProperty(parameterFilter)) {
res.status(500).json({ message: "Selected param not available" });
return;
}
const totalHoursSleep = event.data[parameterFilter] / 60 / 60;
if (minimumSleepTime > 0 && parameterFilter !== "" && totalHoursSleep <= minimumSleepTime) {
// Trigger reschedule
try {
const todayDate = dayjs();
const todayBookings = await prisma.booking.findMany({
where: {
startTime: {
gte: todayDate.startOf("day").toISOString(),
},
endTime: {
lte: todayDate.endOf("day").toISOString(),
},
status: {
in: [BookingStatus.ACCEPTED, BookingStatus.PENDING],
},
// @NOTE: very important filter
userId: credential?.userId,
},
select: {
id: true,
uid: true,
userId: true,
status: true,
},
});
const q = queue({ results: [] });
if (todayBookings.length > 0) {
todayBookings.forEach((booking) =>
q.push(() => {
return Reschedule(booking.uid, "");
})
);
}
await q.start();
} catch (error) {
throw new Error("Failed to reschedule bookings");
}
}
}
} catch (error) {
if (error instanceof Error) {
logger.error(error.message);
}
logger.error("Failed to get sleep score");
}
}
return res.status(200).json({ body: req.body });
} catch (_err) {
const err = getErrorFromUnknown(_err);
console.error(`Webhook Error: ${err.message}`);
res.status(err.statusCode ?? 500).send({
message: err.message,
stack: IS_PRODUCTION ? undefined : err.stack,
});
return;
}
}

View File

@ -0,0 +1,177 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import classNames from "@calcom/lib/classNames";
import showToast from "@calcom/lib/notification";
import { Button, Select } from "@calcom/ui";
export interface IAppConfigurationProps {
credentialIds: number[];
}
const saveSettings = async ({
parameter,
sleepValue,
}: {
parameter: { label: string; value: string };
sleepValue: number;
}) => {
try {
const response = await fetch("/api/integrations/vital/save", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
sleepValue,
parameter: parameter.value,
}),
});
if (response.ok && response.status === 200) {
return true;
}
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
}
}
};
const AppConfiguration = (props: IAppConfigurationProps) => {
const { t } = useTranslation();
const [credentialId] = props.credentialIds;
const options = [
{
label: t("vital_app_total_label", { ns: "vital" }),
value: "total",
},
{
label: t("vital_app_duration_label", { ns: "vital" }),
value: "duration",
},
];
const [selectedParam, setSelectedParam] = useState<{ label: string; value: string }>(options[0]);
const [touchedForm, setTouchedForm] = useState(false);
const defaultSleepValue = 0;
const [sleepValue, setSleepValue] = useState(defaultSleepValue);
const [connected, setConnected] = useState(false);
const [saveLoading, setSaveLoading] = useState(false);
useEffect(() => {
async function getVitalsConfig() {
const response = await fetch("/api/integrations/vital/settings", {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (response.status === 200) {
const vitalSettings: {
connected: boolean;
parameter: string;
sleepValue: number;
} = await response.json();
if (vitalSettings && vitalSettings.connected) {
setConnected(vitalSettings.connected);
}
if (vitalSettings.sleepValue && vitalSettings.parameter) {
const selectedParam = options.find((item) => item.value === vitalSettings.parameter);
if (selectedParam) {
setSelectedParam(selectedParam);
}
setSleepValue(vitalSettings.sleepValue);
}
}
}
getVitalsConfig();
}, []);
if (!credentialId) {
return <></>;
}
const disabledSaveButton = !touchedForm || sleepValue === 0;
return (
<div className="flex-col items-start p-3 text-sm">
<p>
<strong>
{t("connected_vital_app", { ns: "vital" })} Vital App: {connected ? "Yes" : "No"}
</strong>
</p>
<br />
<p>
<strong>{t("vital_app_sleep_automation", { ns: "vital" })}</strong>
</p>
<p className="mt-1">{t("vital_app_automation_description", { ns: "vital" })}</p>
<div className="w-100 mt-2">
<div className="block sm:flex">
<div className="min-w-24 mb-4 mt-5 sm:mb-0">
<label htmlFor="description" className="text-sm font-bold">
{t("vital_app_parameter", { ns: "vital" })}
</label>
</div>
<div className="w-120 mt-2.5">
<Select
options={options}
value={selectedParam}
onChange={(e) => {
e && setSelectedParam(e);
setTouchedForm(true);
}}
/>
</div>
</div>
</div>
<div className="w-full">
<div className="min-w-24 mb-4 mt-3">
<label htmlFor="value" className="text-sm font-bold">
{t("vital_app_trigger", { ns: "vital" })}
</label>
</div>
<div className={"mx-2 mt-0 inline-flex w-24 items-baseline"}>
<input
id="value"
type="text"
pattern="\d*"
maxLength={2}
value={sleepValue}
onChange={(e) => {
setSleepValue(Number(e.currentTarget.value));
setTouchedForm(true);
}}
className={
"pr-12shadow-sm mt-1 block w-full rounded-sm border border-gray-300 py-2 pl-6 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm"
}
/>
<p className="ml-2">
<strong>{t("vital_app_hours", { ns: "vital" })}</strong>
</p>
</div>
</div>
<div>
<Button
className="my-4"
onClick={async () => {
try {
setSaveLoading(true);
await saveSettings({ parameter: selectedParam, sleepValue: sleepValue });
showToast(t("vital_app_save_success"), "success");
} catch (error) {
showToast(t("vital_app_save_error"), "error");
setSaveLoading(false);
}
setTouchedForm(false);
setSaveLoading(false);
}}
loading={saveLoading}
disabled={disabledSaveButton}>
{t("vital_app_save_button", { ns: "vital" })}
</Button>
</div>
</div>
);
};
export default AppConfiguration;

View File

@ -0,0 +1,39 @@
import { useState } from "react";
import { InstallAppButtonProps } from "../../types";
export default function InstallAppButton(props: InstallAppButtonProps) {
const getLinkToken = async () => {
const res = await fetch("/api/integrations/vital/token", {
method: "POST",
body: JSON.stringify({}),
headers: {
"Content-Type": "application/json",
},
});
if (!res.ok) {
throw new Error("Failed to get link token");
}
return await res.json();
};
const [loading, setLoading] = useState(false);
return (
<>
{props.render({
onClick() {
setLoading(true);
getLinkToken()
.then((data) => {
setLoading(false);
window.open(`${data.url}&token=${data.token}`, "_self");
})
.catch((error) => {
setLoading(false);
console.error(error);
});
},
loading: loading,
})}
</>
);
}

View File

@ -0,0 +1,2 @@
export { default as AppConfiguration } from "./AppConfiguration";
export { default as InstallAppButton } from "./InstallAppButton";

View File

@ -0,0 +1,4 @@
export * as api from "./api";
export * as lib from "./lib";
export * as components from "./components";
export { metadata } from "./_metadata";

View File

@ -0,0 +1,34 @@
import { VitalClient } from "@tryvital/vital-node";
import type { ClientConfig } from "@tryvital/vital-node/dist/lib/models";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
type VitalEnv = ClientConfig & {
mode: string;
webhook_secret: string;
};
export let vitalClient: VitalClient | null = null;
export let vitalEnv: VitalEnv | null = null;
export async function initVitalClient(): Promise<VitalClient> {
if (vitalClient) return vitalClient;
const appKeys = (await getAppKeysFromSlug("vital-automation")) as unknown as VitalEnv;
if (
typeof appKeys !== "object" ||
typeof appKeys.api_key !== "string" ||
typeof appKeys.webhook_secret !== "string" ||
typeof appKeys.region !== "string" ||
typeof appKeys.mode !== "string"
)
throw Error("Missing properties in vital-automation DB keys");
vitalEnv = appKeys;
vitalClient = new VitalClient({
region: appKeys.region,
api_key: appKeys.api_key || "",
environment: (appKeys.mode as ClientConfig["environment"]) || "sandbox",
});
return vitalClient;
}
export default vitalClient;

View File

@ -0,0 +1,35 @@
import { CalendarEvent } from "@calcom/types/Calendar";
import AttendeeRequestRescheduledEmail from "./templates/attendee-request-reschedule-email";
import OrganizerRequestRescheduledEmail from "./templates/organizer-request-reschedule-email";
export const sendRequestRescheduleEmail = async (
calEvent: CalendarEvent,
metadata: { rescheduleLink: string }
) => {
const emailsToSend: Promise<unknown>[] = [];
emailsToSend.push(
new Promise((resolve, reject) => {
try {
const requestRescheduleEmail = new AttendeeRequestRescheduledEmail(calEvent, metadata);
resolve(requestRescheduleEmail.sendEmail());
} catch (e) {
reject(console.error("AttendeeRequestRescheduledEmail.sendEmail failed", e));
}
})
);
emailsToSend.push(
new Promise((resolve, reject) => {
try {
const requestRescheduleEmail = new OrganizerRequestRescheduledEmail(calEvent, metadata);
resolve(requestRescheduleEmail.sendEmail());
} catch (e) {
reject(console.error("OrganizerRequestRescheduledEmail.sendEmail failed", e));
}
})
);
await Promise.all(emailsToSend);
};

View File

@ -0,0 +1,34 @@
import SendmailTransport from "nodemailer/lib/sendmail-transport";
import SMTPConnection from "nodemailer/lib/smtp-connection";
function detectTransport(): SendmailTransport.Options | SMTPConnection.Options | string {
if (process.env.EMAIL_SERVER) {
return process.env.EMAIL_SERVER;
}
if (process.env.EMAIL_SERVER_HOST) {
const port = parseInt(process.env.EMAIL_SERVER_PORT!);
const transport = {
host: process.env.EMAIL_SERVER_HOST,
port,
auth: {
user: process.env.EMAIL_SERVER_USER,
pass: process.env.EMAIL_SERVER_PASSWORD,
},
secure: port === 465,
};
return transport;
}
return {
sendmail: true,
newline: "unix",
path: "/usr/sbin/sendmail",
};
}
export const serverConfig = {
transport: detectTransport(),
from: process.env.EMAIL_FROM,
};

View File

@ -0,0 +1 @@
export { default as Reschedule } from "./reschedule";

View File

@ -0,0 +1,170 @@
import { BookingStatus, User, Booking, BookingReference } from "@prisma/client";
import dayjs from "dayjs";
import type { TFunction } from "next-i18next";
import EventManager from "@calcom/core/EventManager";
import { CalendarEventBuilder } from "@calcom/core/builders/CalendarEvent/builder";
import { CalendarEventDirector } from "@calcom/core/builders/CalendarEvent/director";
import { deleteMeeting } from "@calcom/core/videoClient";
import logger from "@calcom/lib/logger";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma from "@calcom/prisma";
import { Person } from "@calcom/types/Calendar";
import { getCalendar } from "../../_utils/getCalendar";
import { sendRequestRescheduleEmail } from "./emailManager";
type PersonAttendeeCommonFields = Pick<User, "id" | "email" | "name" | "locale" | "timeZone" | "username">;
const Reschedule = async (bookingUid: string, cancellationReason: string) => {
const bookingToReschedule = await prisma.booking.findFirst({
select: {
id: true,
uid: true,
title: true,
startTime: true,
endTime: true,
userId: true,
eventTypeId: true,
location: true,
attendees: true,
references: true,
user: {
select: {
id: true,
email: true,
name: true,
timeZone: true,
locale: true,
username: true,
credentials: true,
destinationCalendar: true,
},
},
},
rejectOnNotFound: true,
where: {
uid: bookingUid,
NOT: {
status: {
in: [BookingStatus.CANCELLED, BookingStatus.REJECTED],
},
},
},
});
if (bookingToReschedule && bookingToReschedule.eventTypeId && bookingToReschedule.user) {
const userOwner = bookingToReschedule.user;
const event = await prisma.eventType.findFirst({
select: {
title: true,
users: true,
schedulingType: true,
},
rejectOnNotFound: true,
where: {
id: bookingToReschedule.eventTypeId,
},
});
await prisma.booking.update({
where: {
id: bookingToReschedule.id,
},
data: {
rescheduled: true,
cancellationReason,
status: BookingStatus.CANCELLED,
updatedAt: dayjs().toISOString(),
},
});
const [mainAttendee] = bookingToReschedule.attendees;
// @NOTE: Should we assume attendees language?
const tAttendees = await getTranslation(mainAttendee.locale ?? "en", "common");
const usersToPeopleType = (
users: PersonAttendeeCommonFields[],
selectedLanguage: TFunction
): Person[] => {
return users?.map((user) => {
return {
email: user.email || "",
name: user.name || "",
username: user?.username || "",
language: { translate: selectedLanguage, locale: user.locale || "en" },
timeZone: user?.timeZone,
};
});
};
const userOwnerTranslation = await getTranslation(userOwner.locale ?? "en", "common");
const [userOwnerAsPeopleType] = usersToPeopleType([userOwner], userOwnerTranslation);
const builder = new CalendarEventBuilder();
builder.init({
title: bookingToReschedule.title,
type: event.title,
startTime: bookingToReschedule.startTime.toISOString(),
endTime: bookingToReschedule.endTime.toISOString(),
attendees: usersToPeopleType(
// username field doesn't exists on attendee but could be in the future
bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[],
tAttendees
),
organizer: userOwnerAsPeopleType,
});
const director = new CalendarEventDirector();
director.setBuilder(builder);
director.setExistingBooking(bookingToReschedule as unknown as Booking);
director.setCancellationReason(cancellationReason);
await director.buildForRescheduleEmail();
// Handling calendar and videos cancellation
// This can set previous time as available, until virtual calendar is done
const credentialsMap = new Map();
userOwner.credentials.forEach((credential) => {
credentialsMap.set(credential.type, credential);
});
const bookingRefsFiltered: BookingReference[] = bookingToReschedule.references.filter(
(ref) => !!credentialsMap.get(ref.type)
);
try {
bookingRefsFiltered.forEach((bookingRef) => {
if (bookingRef.uid) {
if (bookingRef.type.endsWith("_calendar")) {
const calendar = getCalendar(credentialsMap.get(bookingRef.type));
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent);
} else if (bookingRef.type.endsWith("_video")) {
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
}
}
});
} catch (error) {
if (error instanceof Error) {
logger.error(error.message);
}
}
// Creating cancelled event as placeholders in calendars, remove when virtual calendar handles it
try {
const eventManager = new EventManager({
credentials: userOwner.credentials,
destinationCalendar: userOwner.destinationCalendar,
});
builder.calendarEvent.title = `Cancelled: ${builder.calendarEvent.title}`;
await eventManager.updateAndSetCancelledPlaceholder(builder.calendarEvent, bookingToReschedule);
} catch (error) {
if (error instanceof Error) {
logger.error(error.message);
}
}
// Send emails
try {
await sendRequestRescheduleEmail(builder.calendarEvent, {
rescheduleLink: builder.rescheduleLink,
});
} catch (error) {
if (error instanceof Error) {
logger.error(error.message);
}
}
return true;
}
};
export default Reschedule;

View File

@ -0,0 +1,208 @@
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import { createEvent, DateArray, Person } from "ics";
import { getCancelLink } from "@calcom/lib/CalEventParser";
import { CalendarEvent } from "@calcom/types/Calendar";
import BaseTemplate from "./base-template";
import {
emailHead,
emailSchedulingBodyHeader,
emailBodyLogo,
emailScheduledBodyHeaderContent,
emailSchedulingBodyDivider,
} from "./common";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(localizedFormat);
dayjs.extend(toArray);
export default class AttendeeRequestRescheduledEmail extends BaseTemplate {
private metadata: { rescheduleLink: string };
constructor(calEvent: CalendarEvent, metadata: { rescheduleLink: string }) {
super(calEvent);
this.metadata = metadata;
}
protected getNodeMailerPayload(): Record<string, unknown> {
const toAddresses = [this.calEvent.attendees[0].email];
return {
icalEvent: {
filename: "event.ics",
content: this.getiCalEventAsString(),
},
from: `Cal.com <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject: `${this.calEvent.organizer.language.translate("requested_to_reschedule_subject_attendee", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
})}`,
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
// @OVERRIDE
protected getiCalEventAsString(): string | undefined {
const icsEvent = createEvent({
start: dayjs(this.calEvent.startTime)
.utc()
.toArray()
.slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc",
productId: "calendso/ics",
title: this.calEvent.organizer.language.translate("ics_event_title", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
}),
description: this.getTextBody(),
duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") },
organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email },
attendees: this.calEvent.attendees.map((attendee: Person) => ({
name: attendee.name,
email: attendee.email,
})),
status: "CANCELLED",
method: "CANCEL",
});
if (icsEvent.error) {
throw icsEvent.error;
}
return icsEvent.value;
}
// @OVERRIDE
protected getWhen(): string {
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.organizer.language.translate("when")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;text-decoration: line-through;">
${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format(
"YYYY"
)} | ${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)} <span style="color: #888888">(${this.getTimezone()})</span>
</p>
</div>`;
}
protected getTextBody(): string {
return `
${this.calEvent.organizer.language.translate("request_reschedule_title_attendee")}
${this.calEvent.organizer.language.translate("request_reschedule_subtitle", {
organizer: this.calEvent.organizer.name,
})},
${this.getWhat()}
${this.getWhen()}
${this.getAdditionalNotes()}
${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")}
${getCancelLink(this.calEvent)}
`.replace(/(<([^>]+)>)/gi, "");
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.organizer.language.translate("rescheduled_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
});
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead(headerContent)}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("calendarCircle")}
${emailScheduledBodyHeaderContent(
this.calEvent.organizer.language.translate("request_reschedule_title_attendee"),
this.calEvent.organizer.language.translate("request_reschedule_subtitle", {
organizer: this.calEvent.organizer.name,
})
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 40px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.getWhat()}
${this.getWhen()}
${this.getWho()}
${this.getAdditionalNotes()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;text-align:center;color:#3E3E3E;">
<a style="padding: 8px 16px;background-color: #292929;color: white;border-radius: 2px;display: inline-block;margin-bottom: 16px;"
href="${this.metadata.rescheduleLink}" target="_blank"
>
Book a new time
<img src="https://app.cal.com/emails/linkIcon.png" style="width:16px; margin-left: 5px;filter: brightness(0) invert(1); vertical-align: top;" />
</a>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
}

View File

@ -0,0 +1,409 @@
import dayjs, { Dayjs } from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import { createEvent, DateArray, Person } from "ics";
import nodemailer from "nodemailer";
import { getAppName } from "@calcom/app-store/utils";
import { getCancelLink, getRichDescription } from "@calcom/lib/CalEventParser";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { serverConfig } from "../emailServerConfig";
import {
emailHead,
emailSchedulingBodyHeader,
emailBodyLogo,
emailScheduledBodyHeaderContent,
emailSchedulingBodyDivider,
linkIcon,
} from "./common";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(localizedFormat);
dayjs.extend(toArray);
export default class OrganizerScheduledEmail {
calEvent: CalendarEvent;
constructor(calEvent: CalendarEvent) {
this.calEvent = calEvent;
}
public sendEmail() {
new Promise((resolve, reject) =>
nodemailer
.createTransport(this.getMailerOptions().transport)
.sendMail(this.getNodeMailerPayload(), (_err, info) => {
if (_err) {
const err = getErrorFromUnknown(_err);
this.printNodeMailerError(err);
reject(err);
} else {
resolve(info);
}
})
).catch((e) => console.error("sendEmail", e));
return new Promise((resolve) => resolve("send mail async"));
}
protected getiCalEventAsString(): string | undefined {
const icsEvent = createEvent({
start: dayjs(this.calEvent.startTime)
.utc()
.toArray()
.slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc",
productId: "calendso/ics",
title: this.calEvent.organizer.language.translate("ics_event_title", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
}),
description: this.getTextBody(),
duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") },
organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email },
attendees: this.calEvent.attendees.map((attendee: Person) => ({
name: attendee.name,
email: attendee.email,
})),
status: "CONFIRMED",
});
if (icsEvent.error) {
throw icsEvent.error;
}
return icsEvent.value;
}
protected getNodeMailerPayload(): Record<string, unknown> {
const toAddresses = [this.calEvent.organizer.email];
if (this.calEvent.team) {
this.calEvent.team.members.forEach((member) => {
const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member);
if (memberAttendee) {
toAddresses.push(memberAttendee.email);
}
});
}
return {
icalEvent: {
filename: "event.ics",
content: this.getiCalEventAsString(),
},
from: `Cal.com <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject: `${this.calEvent.organizer.language.translate("confirmed_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
})}`,
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
protected getMailerOptions() {
return {
transport: serverConfig.transport,
from: serverConfig.from,
};
}
protected getTextBody(): string {
return `
${this.calEvent.organizer.language.translate("new_event_scheduled")}
${this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")}
${getRichDescription(this.calEvent)}
`.trim();
}
protected printNodeMailerError(error: Error): void {
console.error("SEND_BOOKING_CONFIRMATION_ERROR", this.calEvent.organizer.email, error);
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.organizer.language.translate("confirmed_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
});
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead(headerContent)}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("checkCircle")}
${emailScheduledBodyHeaderContent(
this.calEvent.organizer.language.translate("new_event_scheduled"),
this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.getWhat()}
${this.getWhen()}
${this.getWho()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:0px;text-align:left;color:#3E3E3E;">
${this.getManageLink()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
protected getManageLink(): string {
const manageText = this.calEvent.organizer.language.translate("manage_this_event");
return `<p>${this.calEvent.organizer.language.translate(
"need_to_reschedule_or_cancel"
)}</p><p style="font-weight: 400; line-height: 24px;"><a href="${getCancelLink(
this.calEvent
)}" style="color: #3E3E3E;" alt="${manageText}">${manageText}</a></p>`;
}
protected getWhat(): string {
return `
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.organizer.language.translate("what")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.type}</p>
</div>`;
}
protected getWhen(): string {
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.organizer.language.translate("when")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">
${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format(
"YYYY"
)} | ${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)} <span style="color: #888888">(${this.getTimezone()})</span>
</p>
</div>`;
}
protected getWho(): string {
const attendees = this.calEvent.attendees
.map((attendee) => {
return `<div style="color: #494949; font-weight: 400; line-height: 24px;">${
attendee?.name || `${this.calEvent.organizer.language.translate("guest")}`
} <span style="color: #888888"><a href="mailto:${attendee.email}" style="color: #888888;">${
attendee.email
}</a></span></div>`;
})
.join("");
const organizer = `<div style="color: #494949; font-weight: 400; line-height: 24px;">${
this.calEvent.organizer.name
} - ${this.calEvent.organizer.language.translate(
"organizer"
)} <span style="color: #888888"><a href="mailto:${
this.calEvent.organizer.email
}" style="color: #888888;">${this.calEvent.organizer.email}</a></span></div>`;
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.organizer.language.translate("who")}</p>
${organizer + attendees}
</div>`;
}
protected getAdditionalNotes(): string {
if (!this.calEvent.additionalNotes) return "";
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.organizer.language.translate("additional_notes")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px; white-space: pre-wrap;">${
this.calEvent.additionalNotes
}</p>
</div>
`;
}
protected getDescription(): string {
if (!this.calEvent.description) return "";
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.organizer.language.translate("description")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px; white-space: pre-wrap;">${
this.calEvent.description
}</p>
</div>
`;
}
protected getLocation(): string {
let providerName = this.calEvent.location ? getAppName(this.calEvent.location) : "";
if (this.calEvent.location && this.calEvent.location.includes("integrations:")) {
const location = this.calEvent.location.split(":")[1];
providerName = location[0].toUpperCase() + location.slice(1);
}
// If location its a url, probably we should be validating it with a custom library
if (this.calEvent.location && /^https?:\/\//.test(this.calEvent.location)) {
providerName = this.calEvent.location;
}
if (this.calEvent.videoCallData) {
const meetingId = this.calEvent.videoCallData.id;
const meetingPassword = this.calEvent.videoCallData.password;
const meetingUrl = this.calEvent.videoCallData.url;
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.organizer.language.translate("where")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${providerName} ${
meetingUrl &&
`<a href="${meetingUrl}" target="_blank" alt="${this.calEvent.organizer.language.translate(
"meeting_url"
)}"><img src="${linkIcon()}" width="12px"></img></a>`
}</p>
${
meetingId &&
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.organizer.language.translate(
"meeting_id"
)}: <span>${meetingId}</span></div>`
}
${
meetingPassword &&
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.organizer.language.translate(
"meeting_password"
)}: <span>${meetingPassword}</span></div>`
}
${
meetingUrl &&
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.organizer.language.translate(
"meeting_url"
)}: <a href="${meetingUrl}" alt="${this.calEvent.organizer.language.translate(
"meeting_url"
)}" style="color: #3E3E3E" target="_blank">${meetingUrl}</a></div>`
}
</div>
`;
}
if (this.calEvent.additionInformation?.hangoutLink) {
const hangoutLink: string = this.calEvent.additionInformation.hangoutLink;
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.organizer.language.translate("where")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${providerName} ${
hangoutLink &&
`<a href="${hangoutLink}" target="_blank" alt="${this.calEvent.organizer.language.translate(
"meeting_url"
)}"><img src="${linkIcon()}" width="12px"></img></a>`
}</p>
<div style="color: #494949; font-weight: 400; line-height: 24px;"><a href="${hangoutLink}" alt="${this.calEvent.organizer.language.translate(
"meeting_url"
)}" style="color: #3E3E3E" target="_blank">${hangoutLink}</a></div>
</div>
`;
}
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.organizer.language.translate("where")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${
providerName || this.calEvent.location
}</p>
</div>
`;
}
protected getTimezone(): string {
return this.calEvent.organizer.timeZone;
}
protected getOrganizerStart(): Dayjs {
return dayjs(this.calEvent.startTime).tz(this.getTimezone());
}
protected getOrganizerEnd(): Dayjs {
return dayjs(this.calEvent.endTime).tz(this.getTimezone());
}
}

View File

@ -0,0 +1,44 @@
import { IS_PRODUCTION, BASE_URL } from "@lib/config/constants";
export const emailBodyLogo = (): string => {
const image = IS_PRODUCTION
? BASE_URL + "/emails/CalLogo@2x.png"
: "https://app.cal.com/emails/CalLogo@2x.png";
return `
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:32px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:89px;">
<a href="${BASE_URL}" target="_blank">
<img height="19" src="${image}" style="border:0;display:block;outline:none;text-decoration:none;height:19px;width:100%;font-size:13px;" width="89" />
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
`;
};

View File

@ -0,0 +1,91 @@
export const emailHead = (headerContent: string): string => {
return `
<head>
<title>${headerContent}</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Roboto:400,500,700" rel="stylesheet" type="text/css">
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Roboto:400,500,700);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
@media only screen and (max-width:480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
</head>
`;
};

View File

@ -0,0 +1,6 @@
export { emailHead } from "./head";
export { emailSchedulingBodyHeader } from "./scheduling-body-head";
export { emailBodyLogo } from "./body-logo";
export { emailScheduledBodyHeaderContent } from "./scheduling-body-head-content";
export { emailSchedulingBodyDivider } from "./scheduling-body-divider";
export { linkIcon } from "./link-icon";

View File

@ -0,0 +1,5 @@
import { IS_PRODUCTION, BASE_URL } from "@lib/config/constants";
export const linkIcon = (): string => {
return IS_PRODUCTION ? BASE_URL + "/emails/linkIcon.png" : "https://app.cal.com/emails/linkIcon.png";
};

View File

@ -0,0 +1,31 @@
export const emailSchedulingBodyDivider = (): string => {
return `
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:15px 0px 0 0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-bottom:15px;word-break:break-word;">
<p style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 1px #E1E1E1;font-size:1px;margin:0px auto;width:548px;" role="presentation" width="548px" ><tr><td style="height:0;line-height:0;"> &nbsp;
</td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
`;
};

View File

@ -0,0 +1,33 @@
export const emailScheduledBodyHeaderContent = (title: string, subtitle: string): string => {
return `
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:24px;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:24px;font-weight:700;line-height:24px;text-align:center;color:#292929;">${title}</div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:center;color:#494949;">${subtitle}</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
`;
};

View File

@ -0,0 +1,71 @@
import { IS_PRODUCTION, BASE_URL } from "@lib/config/constants";
export type BodyHeadType = "checkCircle" | "xCircle" | "calendarCircle";
export const getHeadImage = (headerType: BodyHeadType): string => {
switch (headerType) {
case "checkCircle":
return IS_PRODUCTION
? BASE_URL + "/emails/checkCircle@2x.png"
: "https://app.cal.com/emails/checkCircle@2x.png";
case "xCircle":
return IS_PRODUCTION
? BASE_URL + "/emails/xCircle@2x.png"
: "https://app.cal.com/emails/xCircle@2x.png";
case "calendarCircle":
return IS_PRODUCTION
? BASE_URL + "/emails/calendarCircle@2x.png"
: "https://app.cal.com/emails/calendarCircle@2x.png";
}
};
export const emailSchedulingBodyHeader = (headerType: BodyHeadType): string => {
const image = getHeadImage(headerType);
return `
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0px;padding-top:40px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;border-top:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:30px 30px 0 30px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:558px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:64px;">
<img height="64" src="${image}" style="border:0;display:block;outline:none;text-decoration:none;height:64px;width:100%;font-size:13px;" width="64" />
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
`;
};

View File

@ -0,0 +1,188 @@
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import { createEvent, DateArray, Person } from "ics";
import { getCancelLink } from "@calcom/lib/CalEventParser";
import { CalendarEvent } from "@calcom/types/Calendar";
import BaseTemplate from "./base-template";
import {
emailHead,
emailSchedulingBodyHeader,
emailBodyLogo,
emailScheduledBodyHeaderContent,
emailSchedulingBodyDivider,
} from "./common";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(localizedFormat);
dayjs.extend(toArray);
export default class OrganizerRequestRescheduledEmail extends BaseTemplate {
private metadata: { rescheduleLink: string };
constructor(calEvent: CalendarEvent, metadata: { rescheduleLink: string }) {
super(calEvent);
this.metadata = metadata;
}
protected getNodeMailerPayload(): Record<string, unknown> {
const toAddresses = [this.calEvent.organizer.email];
return {
icalEvent: {
filename: "event.ics",
content: this.getiCalEventAsString(),
},
from: `Cal.com <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject: `${this.calEvent.organizer.language.translate("rescheduled_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
})}`,
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
// @OVERRIDE
protected getiCalEventAsString(): string | undefined {
const icsEvent = createEvent({
start: dayjs(this.calEvent.startTime)
.utc()
.toArray()
.slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc",
productId: "calendso/ics",
title: this.calEvent.organizer.language.translate("ics_event_title", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
}),
description: this.getTextBody(),
duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") },
organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email },
attendees: this.calEvent.attendees.map((attendee: Person) => ({
name: attendee.name,
email: attendee.email,
})),
status: "CANCELLED",
method: "CANCEL",
});
if (icsEvent.error) {
throw icsEvent.error;
}
return icsEvent.value;
}
// @OVERRIDE
protected getWhen(): string {
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.organizer.language.translate("when")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;text-decoration: line-through;">
${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format(
"YYYY"
)} | ${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)} <span style="color: #888888">(${this.getTimezone()})</span>
</p>
</div>`;
}
protected getTextBody(): string {
return `
${this.calEvent.organizer.language.translate("request_reschedule_title_organizer", {
attendee: this.calEvent.attendees[0].name,
})}
${this.calEvent.organizer.language.translate("request_reschedule_subtitle_organizer", {
attendee: this.calEvent.attendees[0].name,
})},
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
${this.getAdditionalNotes()}
${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")}
${getCancelLink(this.calEvent)}
`.replace(/(<([^>]+)>)/gi, "");
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.organizer.language.translate("rescheduled_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
});
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead(headerContent)}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("calendarCircle")}
${emailScheduledBodyHeaderContent(
this.calEvent.organizer.language.translate("request_reschedule_title_organizer", {
attendee: this.calEvent.attendees[0].name,
}),
this.calEvent.organizer.language.translate("request_reschedule_subtitle_organizer", {
attendee: this.calEvent.attendees[0].name,
})
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 40px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.getWhat()}
${this.getWhen()}
${this.getWho()}
${this.getAdditionalNotes()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
}

View File

@ -0,0 +1,16 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/vital",
"version": "0.1.0",
"main": "./index.ts",
"description": "Connect your health data or wearables to trigger actions on your calendar.",
"dependencies": {
"@calcom/prisma": "*",
"@tryvital/vital-node": "^1.3.6",
"queue": "^6.0.2"
},
"devDependencies": {
"@calcom/types": "*"
}
}

View File

@ -0,0 +1,4 @@
<svg width="372" height="314" viewBox="0 0 372 314" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M185.53 72.5053C204.281 44.0924 258.928 -4.57799 318.669 35.9042C378.41 76.3865 348.801 138.092 326.53 163.885" stroke="black" stroke-width="35" stroke-linecap="round" stroke-dasharray="1 70"/>
<path d="M327.512 163.093L192.873 293.229C188.975 296.997 182.784 296.972 178.916 293.173L46.4675 163.093C28.5125 147.494 -2.90857 98.1249 38.2381 47.345C79.3848 -3.43492 141.978 15.9809 185.868 72.735" stroke="black" stroke-width="35" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 571 B

View File

@ -1,7 +1,7 @@
import { useTranslation } from "next-i18next";
export const useLocale = () => {
const { i18n, t } = useTranslation("common");
export const useLocale = (namespace: Parameters<typeof useTranslation>[0] = "common") => {
const { i18n, t } = useTranslation(namespace);
return {
i18n,

View File

@ -5,6 +5,7 @@ import prisma from ".";
require("dotenv").config({ path: "../../.env.appStore" });
async function createApp(
/** The App identifier in the DB also used for public page in `/apps/[slug]` */
slug: Prisma.AppCreateInput["slug"],
/** The directory name for `/packages/app-store/[dirName]` */
dirName: Prisma.AppCreateInput["dirName"],
@ -86,6 +87,14 @@ async function main() {
});
}
await createApp("space-booking", "spacebooking", ["other"], "spacebooking_other");
if (process.env.VITAL_API_KEY && process.env.VITAL_WEBHOOK_SECRET) {
await createApp("vital-automation", "vital", ["other"], "vital_other", {
mode: process.env.VITAL_DEVELOPMENT_MODE || "sandbox",
region: process.env.VITAL_REGION || "us",
api_key: process.env.VITAL_API_KEY,
webhook_secret: process.env.VITAL_WEBHOOK_SECRET,
});
}
await createApp("zapier", "zapier", ["other"], "zapier_other");
// Web3 apps
await createApp("huddle01", "huddle01video", ["web3", "video"], "huddle01_video");

View File

@ -0,0 +1,63 @@
import ReactSelect, { components, GroupBase, InputProps, Props } from "react-select";
import classNames from "@calcom/lib/classNames";
export type SelectProps<
Option,
IsMulti extends boolean = false,
Group extends GroupBase<Option> = GroupBase<Option>
> = Props<Option, IsMulti, Group>;
export const InputComponent = <Option, IsMulti extends boolean, Group extends GroupBase<Option>>({
inputClassName,
...props
}: InputProps<Option, IsMulti, Group>) => {
return (
<components.Input
// disables our default form focus hightlight on the react-select input element
inputClassName={classNames("focus:ring-0 focus:ring-offset-0", inputClassName)}
{...props}
/>
);
};
function Select<
Option,
IsMulti extends boolean = false,
Group extends GroupBase<Option> = GroupBase<Option>
>({ className, ...props }: SelectProps<Option, IsMulti, Group>) {
return (
<ReactSelect
theme={(theme) => ({
...theme,
borderRadius: 2,
colors: {
...theme.colors,
primary: "var(--brand-color)",
primary50: "rgba(209 , 213, 219, var(--tw-bg-opacity))",
primary25: "rgba(244, 245, 246, var(--tw-bg-opacity))",
},
})}
styles={{
option: (provided, state) => ({
...provided,
color: state.isSelected ? "var(--brand-text-color)" : "black",
":active": {
backgroundColor: state.isSelected ? "" : "var(--brand-color)",
color: "var(--brand-text-color)",
},
}),
}}
components={{
...components,
IndicatorSeparator: () => null,
Input: InputComponent,
}}
className={classNames("text-sm shadow-sm", className)}
{...props}
/>
);
}
export default Select;

View File

@ -1,4 +1,5 @@
export { default as Button } from "./Button";
export { default as EmptyScreen } from "./EmptyScreen";
export { default as Switch } from "./Switch";
export { default as Select } from "./form/Select";
export * from "./skeleton";
export { default as Switch } from "./Switch";

471
yarn.lock
View File

@ -519,6 +519,13 @@
dependencies:
"@babel/helper-plugin-utils" "^7.16.7"
"@babel/plugin-syntax-typescript@^7.7.2":
version "7.17.10"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.17.10.tgz#80031e6042cad6a95ed753f672ebd23c30933195"
integrity sha512-xJefea1DWXW09pW4Tm9bjwVlPDyYA2it3fWlmEjpYz6alPvTUjL0EOzNzI/FEOyI3r4/J7uVH5UqKgl1TQ5hqQ==
dependencies:
"@babel/helper-plugin-utils" "^7.16.7"
"@babel/plugin-transform-modules-commonjs@7.16.8":
version "7.16.8"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.16.8.tgz#cdee19aae887b16b9d331009aa9a219af7c86afe"
@ -1143,9 +1150,9 @@
integrity sha512-aaRnYxBb3MU2FNJf3Ut9RMTUqqU3as0aI1lQhgo2n9Fa67wRu14iOGqx93xB+uMNVfNwZ5B3y/Ndm7qZGuFeMQ==
"@headlessui/react@^1.5.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.6.0.tgz#5943c0b1e5b1a02566ab45f665a05188b1d079c5"
integrity sha512-PlDuytBC6iDC/uMvpANm5VpRSuayyXMEeo/dNIwAZNHCfhZUqDQgLXjGu48SHsvMw22Kc3c3u9TOAMZNg+1vzw==
version "1.6.1"
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.6.1.tgz#d822792e589aac005462491dd62f86095e0c3bef"
integrity sha512-gMd6uIs1U4Oz718Z5gFoV0o/vD43/4zvbyiJN9Dt7PK9Ubxn+TmJwTmYwyNJc5KxxU1t0CmgTNgwZX9+4NjCnQ==
"@heroicons/react@^1.0.4", "@heroicons/react@^1.0.6":
version "1.0.6"
@ -2837,6 +2844,16 @@
resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.3.tgz#1185726610acc37317ddab11c3c7f9066966bd20"
integrity sha512-O3uyB/JbkAEMZaP3YqyHH7TMnex7tWyCbCI4EfJdOCoN6HIhqdJBWTM6aCCiWQ/5f5wxjgU735QAIpJbjDvmzg==
"@stablelib/base64@^1.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@stablelib/base64/-/base64-1.0.1.tgz#bdfc1c6d3a62d7a3b7bbc65b6cce1bb4561641be"
integrity sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==
"@stablelib/utf8@^1.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@stablelib/utf8/-/utf8-1.0.1.tgz#fd5e7ad353b7e3cd1ce85e49099360e57693cfd1"
integrity sha512-FrYD1xadah/TtAP6VJ04lDD5h9rdDj/d8wH/jMYTtHqZBv9z2btdvEU8vTxdjdkFmo1b/BH+t3R1wi/mYhCCNg==
"@stripe/react-stripe-js@^1.4.1":
version "1.7.0"
resolved "https://registry.yarnpkg.com/@stripe/react-stripe-js/-/react-stripe-js-1.7.0.tgz#83c993a09a903703205d556617f9729784a896c3"
@ -2917,6 +2934,17 @@
resolved "https://registry.yarnpkg.com/@trpc/server/-/server-9.22.0.tgz#5c6442666efd774069af938016a7ab6def32f746"
integrity sha512-MwMVLjXXeTFKdGtFQokI6odGZifMBvMpfaddqg3RJp2wcERzTm1A33MB5mmCxH9opjmPjqMMkT/3tRunRFaitw==
"@tryvital/vital-node@^1.3.6":
version "1.3.6"
resolved "https://registry.yarnpkg.com/@tryvital/vital-node/-/vital-node-1.3.6.tgz#8919c02c24969edddcd41be8c253788f76b769d9"
integrity sha512-RcUsiS/+DiQkv3jDU54ECorLKEmxi0FmVLa0scPnehO/sZgzc9356oAdKF7cglAie3+yxUAYrHlIfaebHU8yNA==
dependencies:
auth0 "^2.35.1"
axios ">=0.21.2"
axios-retry "^3.2.4"
crypto "^1.0.1"
svix "0.42.3"
"@ts-morph/common@~0.12.3":
version "0.12.3"
resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.12.3.tgz#a96e250217cd30e480ab22ec6a0ebbe65fd784ff"
@ -3021,6 +3049,14 @@
dependencies:
"@types/node" "*"
"@types/body-parser@*":
version "1.19.2"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0"
integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==
dependencies:
"@types/connect" "*"
"@types/node" "*"
"@types/chrome@^0.0.136":
version "0.0.136"
resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.136.tgz#7c011b9f997b0156f25a140188a0c5689d3f368f"
@ -3029,6 +3065,13 @@
"@types/filesystem" "*"
"@types/har-format" "*"
"@types/connect@*":
version "3.4.35"
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1"
integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==
dependencies:
"@types/node" "*"
"@types/cross-spawn@6.0.2":
version "6.0.2"
resolved "https://registry.yarnpkg.com/@types/cross-spawn/-/cross-spawn-6.0.2.tgz#168309de311cd30a2b8ae720de6475c2fbf33ac7"
@ -3070,6 +3113,40 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83"
integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==
"@types/express-jwt@0.0.42":
version "0.0.42"
resolved "https://registry.yarnpkg.com/@types/express-jwt/-/express-jwt-0.0.42.tgz#4f04e1fadf9d18725950dc041808a4a4adf7f5ae"
integrity sha512-WszgUddvM1t5dPpJ3LhWNH8kfNN8GPIBrAGxgIYXVCEGx6Bx4A036aAuf/r5WH9DIEdlmp7gHOYvSM6U87B0ag==
dependencies:
"@types/express" "*"
"@types/express-unless" "*"
"@types/express-serve-static-core@^4.17.18":
version "4.17.28"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz#c47def9f34ec81dc6328d0b1b5303d1ec98d86b8"
integrity sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==
dependencies:
"@types/node" "*"
"@types/qs" "*"
"@types/range-parser" "*"
"@types/express-unless@*":
version "0.5.3"
resolved "https://registry.yarnpkg.com/@types/express-unless/-/express-unless-0.5.3.tgz#271f8603617445568ed0d6efe25a7d2f338544c1"
integrity sha512-TyPLQaF6w8UlWdv4gj8i46B+INBVzURBNRahCozCSXfsK2VTlL1wNyTlMKw817VHygBtlcl5jfnPadlydr06Yw==
dependencies:
"@types/express" "*"
"@types/express@*":
version "4.17.13"
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034"
integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==
dependencies:
"@types/body-parser" "*"
"@types/express-serve-static-core" "^4.17.18"
"@types/qs" "*"
"@types/serve-static" "*"
"@types/filesystem@*":
version "0.0.32"
resolved "https://registry.yarnpkg.com/@types/filesystem/-/filesystem-0.0.32.tgz#307df7cc084a2293c3c1a31151b178063e0a8edf"
@ -3197,6 +3274,11 @@
resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.1.tgz#d9ba43490fa3a3df958759adf69396c3532cf2c1"
integrity sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==
"@types/mime@^1":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
"@types/module-alias@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/module-alias/-/module-alias-2.0.1.tgz#e5893236ce922152d57c5f3f978f764f4deeb45f"
@ -3278,6 +3360,16 @@
dependencies:
"@types/node" "*"
"@types/qs@*":
version "6.9.7"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==
"@types/range-parser@*":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
"@types/react-calendar@^3.0.0":
version "3.5.0"
resolved "https://registry.yarnpkg.com/@types/react-calendar/-/react-calendar-3.5.0.tgz#8195a33e20395e1f5171eea9c80156f3a5115b43"
@ -3927,6 +4019,19 @@ atob@^2.1.2:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
auth0@^2.35.1:
version "2.40.0"
resolved "https://registry.yarnpkg.com/auth0/-/auth0-2.40.0.tgz#bc3bb047589e7ba93a729dd9973fcabe6c175dcd"
integrity sha512-xrnyLpKw+BP7u6OqHMYcSkLA2yDbTDefMeqArpwAU8UG5MPkhsTFQGPMXLzxHr2M5mV+elJOQ6w1acSY/2uRbA==
dependencies:
axios "^0.25.0"
form-data "^3.0.1"
jsonwebtoken "^8.5.1"
jwks-rsa "^1.12.1"
lru-memoizer "^2.1.4"
rest-facade "^1.16.3"
retry "^0.13.1"
autolinker@^3.11.0:
version "3.15.0"
resolved "https://registry.yarnpkg.com/autolinker/-/autolinker-3.15.0.tgz#03956088648f236642a5783612f9ca16adbbed38"
@ -3947,12 +4052,12 @@ autoprefixer@^10.4.0, autoprefixer@^10.4.4:
postcss-value-parser "^4.2.0"
autoprefixer@^10.4.2:
version "10.4.5"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.5.tgz#662193c744094b53d3637f39be477e07bd904998"
integrity sha512-Fvd8yCoA7lNX/OUllvS+aS1I7WRBclGXsepbvT8ZaPgrH24rgXpZzF0/6Hh3ZEkwg+0AES/Osd196VZmYoEFtw==
version "10.4.7"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.7.tgz#1db8d195f41a52ca5069b7593be167618edbbedf"
integrity sha512-ypHju4Y2Oav95SipEcCcI5J7CGPuvz8oat7sUtYj3ClK44bldfvtvcxK6IEK++7rqB7YchDGzweZIBG+SD0ZAA==
dependencies:
browserslist "^4.20.2"
caniuse-lite "^1.0.30001332"
browserslist "^4.20.3"
caniuse-lite "^1.0.30001335"
fraction.js "^4.2.0"
normalize-range "^0.1.2"
picocolors "^1.0.0"
@ -3978,13 +4083,35 @@ axe-core@^4.3.5:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.1.tgz#7dbdc25989298f9ad006645cd396782443757413"
integrity sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw==
axios@^0.26.1:
axios-retry@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/axios-retry/-/axios-retry-3.2.4.tgz#f447a53c3456f5bfeca18f20c3a3272207d082ae"
integrity sha512-Co3UXiv4npi6lM963mfnuH90/YFLKWWDmoBYfxkHT5xtkSSWNqK9zdG3fw5/CP/dsoKB5aMMJCsgab+tp1OxLQ==
dependencies:
"@babel/runtime" "^7.15.4"
is-retry-allowed "^2.2.0"
axios@>=0.21.2, axios@^0.26.1:
version "0.26.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9"
integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==
dependencies:
follow-redirects "^1.14.8"
axios@^0.21.1:
version "0.21.4"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"
integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==
dependencies:
follow-redirects "^1.14.0"
axios@^0.25.0:
version "0.25.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a"
integrity sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==
dependencies:
follow-redirects "^1.14.7"
axobject-query@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
@ -4437,6 +4564,17 @@ browserslist@^4.20.2:
node-releases "^2.0.2"
picocolors "^1.0.0"
browserslist@^4.20.3:
version "4.20.3"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.3.tgz#eb7572f49ec430e054f56d52ff0ebe9be915f8bf"
integrity sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==
dependencies:
caniuse-lite "^1.0.30001332"
electron-to-chromium "^1.4.118"
escalade "^3.1.1"
node-releases "^2.0.3"
picocolors "^1.0.0"
bs-logger@0.x:
version "0.2.6"
resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8"
@ -4629,6 +4767,14 @@ callsites@^3.0.0:
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
camel-case@^1.1.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-1.2.2.tgz#1aca7c4d195359a2ce9955793433c6e5542511f2"
integrity sha1-Gsp8TRlTWaLOmVV5NDPG5VQlEfI=
dependencies:
sentence-case "^1.1.1"
upper-case "^1.1.1"
camelcase-css@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
@ -4649,10 +4795,10 @@ caniuse-lite@^1.0.30001283, caniuse-lite@^1.0.30001313, caniuse-lite@^1.0.300013
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001325.tgz#2b4ad19b77aa36f61f2eaf72e636d7481d55e606"
integrity sha512-sB1bZHjseSjDtijV1Hb7PB2Zd58Kyx+n/9EotvZ4Qcz2K3d0lWB8dB4nb8wN/TsOGFq3UuAm0zQZNQ4SoR7TrQ==
caniuse-lite@^1.0.30001332:
version "1.0.30001334"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001334.tgz#892e9965b35285033fc2b8a8eff499fe02f13d8b"
integrity sha512-kbaCEBRRVSoeNs74sCuq92MJyGrMtjWVfhltoHUCW4t4pXFvGjUBrfo47weBRViHkiV3eBYyIsfl956NtHGazw==
caniuse-lite@^1.0.30001332, caniuse-lite@^1.0.30001335:
version "1.0.30001335"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001335.tgz#899254a0b70579e5a957c32dced79f0727c61f2a"
integrity sha512-ddP1Tgm7z2iIxu6QTtbZUv6HJxSaV/PZeSrWFZtbY4JZ69tOeNhBCl3HyRQgeNZKE5AOn1kpV7fhljigy0Ty3w==
capture-exit@^2.0.0:
version "2.0.0"
@ -4713,6 +4859,28 @@ chalk@^2.0.0, chalk@^2.4.1:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
change-case@^2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/change-case/-/change-case-2.3.1.tgz#2c4fde3f063bb41d00cd68e0d5a09db61cbe894f"
integrity sha1-LE/ePwY7tB0AzWjg1aCdthy+iU8=
dependencies:
camel-case "^1.1.1"
constant-case "^1.1.0"
dot-case "^1.1.0"
is-lower-case "^1.1.0"
is-upper-case "^1.1.0"
lower-case "^1.1.1"
lower-case-first "^1.0.0"
param-case "^1.1.0"
pascal-case "^1.1.0"
path-case "^1.1.0"
sentence-case "^1.1.1"
snake-case "^1.1.0"
swap-case "^1.1.0"
title-case "^1.1.0"
upper-case "^1.1.1"
upper-case-first "^1.1.0"
char-regex@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
@ -5041,7 +5209,7 @@ commander@^6.2.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
component-emitter@^1.2.1:
component-emitter@^1.2.1, component-emitter@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
@ -5104,7 +5272,7 @@ cookie@0.4.2, cookie@^0.4.1, cookie@~0.4.1:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==
cookiejar@^2.1.1:
cookiejar@^2.1.1, cookiejar@^2.1.2:
version "2.1.3"
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc"
integrity sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==
@ -5273,6 +5441,11 @@ crypto-browserify@3.12.0, crypto-browserify@^3.11.0:
randombytes "^2.0.0"
randomfill "^1.0.3"
crypto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/crypto/-/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037"
integrity sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==
css.escape@1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb"
@ -5466,6 +5639,11 @@ deep-is@^0.1.3, deep-is@~0.1.3:
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
deepmerge@^3.2.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-3.3.0.tgz#d3c47fd6f3a93d517b14426b0628a17b0125f5f7"
integrity sha512-GRQOafGHwMHpjPx9iCvTgpu9NojZ49q794EEL94JVEw6VaeA8XTUyBKvAkOOjBX9oJNiV6G3P+T+tihFjo2TqA==
deepmerge@^4.2.2, deepmerge@~4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
@ -5701,6 +5879,13 @@ dompurify@=2.3.3:
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.3.tgz#c1af3eb88be47324432964d8abc75cf4b98d634c"
integrity sha512-dqnqRkPMAjOZE0FogZ+ceJNM2dZ3V/yNOuFB7+39qpO93hHhfRpHw3heYQC7DPK9FqbQTfBKUJhiSfz4MvXYwg==
dot-case@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-1.1.2.tgz#1e73826900de28d6de5480bc1de31d0842b06bec"
integrity sha1-HnOCaQDeKNbeVIC8HeMdCEKwa+w=
dependencies:
sentence-case "^1.1.2"
dotenv-checker@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/dotenv-checker/-/dotenv-checker-1.1.5.tgz#0bdd140ce50c560d70fb8103ffe1a14c9ad65c74"
@ -5775,6 +5960,11 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
electron-to-chromium@^1.4.118:
version "1.4.132"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.132.tgz#b64599eb018221e52e2e4129de103b03a413c55d"
integrity sha512-JYdZUw/1068NWN+SwXQ7w6Ue0bWYGihvSUNNQwurvcDV/SM7vSiGZ3NuFvFgoEiCs4kB8xs3cX2an3wB7d4TBw==
electron-to-chromium@^1.4.76, electron-to-chromium@^1.4.84:
version "1.4.103"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.103.tgz#abfe376a4d70fa1e1b4b353b95df5d6dfd05da3a"
@ -6865,11 +7055,16 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
fast-safe-stringify@^2.0.6:
fast-safe-stringify@^2.0.6, fast-safe-stringify@^2.0.7:
version "2.1.1"
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
fast-sha256@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/fast-sha256/-/fast-sha256-1.3.0.tgz#7916ba2054eeb255982608cccd0f6660c79b7ae6"
integrity sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==
fast-text-encoding@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53"
@ -7029,7 +7224,7 @@ focus-visible@^5.1.0:
resolved "https://registry.yarnpkg.com/focus-visible/-/focus-visible-5.2.0.tgz#3a9e41fccf587bd25dcc2ef045508284f0a4d6b3"
integrity sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ==
follow-redirects@^1.14.8:
follow-redirects@^1.14.0, follow-redirects@^1.14.7, follow-redirects@^1.14.8:
version "1.14.9"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7"
integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==
@ -7072,7 +7267,7 @@ form-data@^2.5.0:
combined-stream "^1.0.6"
mime-types "^2.1.12"
form-data@^3.0.0:
form-data@^3.0.0, form-data@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f"
integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==
@ -7103,6 +7298,11 @@ formdata-node@^4.0.0:
node-domexception "1.0.0"
web-streams-polyfill "4.0.0-beta.1"
formidable@^1.2.2:
version "1.2.6"
resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168"
integrity sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==
forwarded@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
@ -8356,6 +8556,13 @@ is-interactive@^1.0.0:
resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e"
integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==
is-lower-case@^1.1.0:
version "1.1.3"
resolved "https://registry.yarnpkg.com/is-lower-case/-/is-lower-case-1.1.3.tgz#7e147be4768dc466db3bfb21cc60b31e6ad69393"
integrity sha1-fhR75HaNxGbbO/shzGCzHmrWk5M=
dependencies:
lower-case "^1.1.0"
is-natural-number@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8"
@ -8462,6 +8669,11 @@ is-retry-allowed@^1.0.0, is-retry-allowed@^1.1.0:
resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4"
integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==
is-retry-allowed@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz#88f34cbd236e043e71b6932d09b0c65fb7b4d71d"
integrity sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==
is-shared-array-buffer@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79"
@ -8514,6 +8726,13 @@ is-unicode-supported@^0.1.0:
resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
is-upper-case@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/is-upper-case/-/is-upper-case-1.1.2.tgz#8d0b1fa7e7933a1e58483600ec7d9661cbaf756f"
integrity sha1-jQsfp+eTOh5YSDYA7H2WYcuvdW8=
dependencies:
upper-case "^1.1.0"
is-weakref@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2"
@ -8611,6 +8830,17 @@ istanbul-lib-instrument@^5.0.4:
istanbul-lib-coverage "^3.2.0"
semver "^6.3.0"
istanbul-lib-instrument@^5.1.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.0.tgz#31d18bdd127f825dd02ea7bfdfd906f8ab840e9f"
integrity sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A==
dependencies:
"@babel/core" "^7.12.3"
"@babel/parser" "^7.14.7"
"@istanbuljs/schema" "^0.1.2"
istanbul-lib-coverage "^3.2.0"
semver "^6.3.0"
istanbul-lib-report@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6"
@ -9333,6 +9563,15 @@ jsprim@^1.2.2:
array-includes "^3.1.4"
object.assign "^4.1.2"
jwa@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==
dependencies:
buffer-equal-constant-time "1.0.1"
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
jwa@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc"
@ -9342,6 +9581,30 @@ jwa@^2.0.0:
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
jwks-rsa@^1.12.1:
version "1.12.3"
resolved "https://registry.yarnpkg.com/jwks-rsa/-/jwks-rsa-1.12.3.tgz#40232f85d16734cb82837f38bb3e350a34435400"
integrity sha512-cFipFDeYYaO9FhhYJcZWX/IyZgc0+g316rcHnDpT2dNRNIE/lMOmWKKqp09TkJoYlNFzrEVODsR4GgXJMgWhnA==
dependencies:
"@types/express-jwt" "0.0.42"
axios "^0.21.1"
debug "^4.1.0"
http-proxy-agent "^4.0.1"
https-proxy-agent "^5.0.0"
jsonwebtoken "^8.5.1"
limiter "^1.1.5"
lru-memoizer "^2.1.2"
ms "^2.1.2"
proxy-from-env "^1.1.0"
jws@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
dependencies:
jwa "^1.4.1"
safe-buffer "^5.0.1"
jws@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4"
@ -9540,6 +9803,11 @@ lilconfig@^2.0.5:
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.5.tgz#19e57fd06ccc3848fd1891655b5a447092225b25"
integrity sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==
limiter@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2"
integrity sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==
lines-and-columns@^1.1.6:
version "1.2.4"
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
@ -9652,6 +9920,11 @@ lodash.isplainobject@^4.0.6:
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
lodash.isstring@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
@ -9707,6 +9980,18 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
lower-case-first@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/lower-case-first/-/lower-case-first-1.0.2.tgz#e5da7c26f29a7073be02d52bac9980e5922adfa1"
integrity sha1-5dp8JvKacHO+AtUrrJmA5ZIq36E=
dependencies:
lower-case "^1.1.2"
lower-case@^1.1.0, lower-case@^1.1.1, lower-case@^1.1.2:
version "1.1.4"
resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac"
integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw=
lowercase-keys@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306"
@ -10506,6 +10791,11 @@ mime@3.0.0:
resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7"
integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==
mime@^2.4.6:
version "2.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367"
integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==
mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
@ -10674,7 +10964,7 @@ ms@2.1.2:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
ms@2.1.3, ms@^2.1.1:
ms@2.1.3, ms@^2.1.1, ms@^2.1.2:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
@ -10776,6 +11066,11 @@ nanoid@^3.1.23, nanoid@^3.1.30, nanoid@^3.3.1:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.2.tgz#c89622fafb4381cd221421c69ec58547a1eec557"
integrity sha512-CuHBogktKwpm5g2sRgv83jEy2ijFzBwMoYA60orPDR7ynsLijJDqgsi4RDGj3OJpy3Ieb+LYwiRmIOGyytgITA==
nanoid@^3.3.3:
version "3.3.4"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
nanomatch@^1.2.9:
version "1.2.13"
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
@ -11039,6 +11334,11 @@ node-releases@^2.0.2:
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.2.tgz#7139fe71e2f4f11b47d4d2986aaf8c48699e0c01"
integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==
node-releases@^2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.4.tgz#f38252370c43854dc48aa431c766c6c398f40476"
integrity sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==
nodemailer@^6.7.2:
version "6.7.3"
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.3.tgz#b73f9a81b9c8fa8acb4ea14b608f5e725ea8e018"
@ -11526,6 +11826,13 @@ pako@^1.0.5:
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
param-case@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/param-case/-/param-case-1.1.2.tgz#dcb091a43c259b9228f1c341e7b6a44ea0bf9743"
integrity sha1-3LCRpDwlm5Io8cNB57akTqC/l0M=
dependencies:
sentence-case "^1.1.2"
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
@ -11643,6 +11950,14 @@ parseurl@~1.3.3:
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
pascal-case@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-1.1.2.tgz#3e5d64a20043830a7c49344c2d74b41be0c9c99b"
integrity sha1-Pl1kogBDgwp8STRMLXS0G+DJyZs=
dependencies:
camel-case "^1.1.1"
upper-case-first "^1.1.0"
pascalcase@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
@ -11653,6 +11968,13 @@ path-browserify@^1.0.1:
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
path-case@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/path-case/-/path-case-1.1.2.tgz#50ce6ba0d3bed3dd0b5c2a9c4553697434409514"
integrity sha1-UM5roNO+090LXCqcRVNpdDRAlRQ=
dependencies:
sentence-case "^1.1.2"
path-exists@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
@ -12013,7 +12335,16 @@ postcss@8.4.5:
picocolors "^1.0.0"
source-map-js "^1.0.1"
postcss@^8.3.6, postcss@^8.4.12, postcss@^8.4.4, postcss@^8.4.6, postcss@^8.4.8:
postcss@^8.3.6, postcss@^8.4.8:
version "8.4.13"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.13.tgz#7c87bc268e79f7f86524235821dfdf9f73e5d575"
integrity sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA==
dependencies:
nanoid "^3.3.3"
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.4.12, postcss@^8.4.4, postcss@^8.4.6:
version "8.4.12"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.12.tgz#1e7de78733b28970fa4743f7da6f3763648b1905"
integrity sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg==
@ -12231,7 +12562,7 @@ prr@~1.0.1:
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY=
pseudomap@^1.0.2:
pseudomap@^1.0.1, pseudomap@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
@ -12298,7 +12629,7 @@ qrcode@^1.5.0:
pngjs "^5.0.0"
yargs "^15.3.1"
qs@6.10.3, qs@^6.10.2, qs@^6.10.3, qs@^6.7.0:
qs@6.10.3, qs@^6.10.2, qs@^6.10.3, qs@^6.7.0, qs@^6.9.4:
version "6.10.3"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e"
integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==
@ -13102,6 +13433,16 @@ responselike@1.0.2, responselike@^1.0.2:
dependencies:
lowercase-keys "^1.0.0"
rest-facade@^1.16.3:
version "1.16.3"
resolved "https://registry.yarnpkg.com/rest-facade/-/rest-facade-1.16.3.tgz#e4d4b44a4f6a4268f30f02e1f544dd49b040499e"
integrity sha512-9BQTPLiwg23XZwcWi0ys1wTizfc//0b2G3U6vBWcgqh56ozs2K6CD+Jw4DYcw3AqdPQN7jj8nzRUcUXFVGzb0Q==
dependencies:
change-case "^2.3.0"
deepmerge "^3.2.0"
lodash.get "^4.4.2"
superagent "^5.1.1"
restore-cursor@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
@ -13360,6 +13701,13 @@ send@0.17.2:
range-parser "~1.2.1"
statuses "~1.5.0"
sentence-case@^1.1.1, sentence-case@^1.1.2:
version "1.1.3"
resolved "https://registry.yarnpkg.com/sentence-case/-/sentence-case-1.1.3.tgz#8034aafc2145772d3abe1509aa42c9e1042dc139"
integrity sha1-gDSq/CFFdy06vhUJqkLJ4QQtwTk=
dependencies:
lower-case "^1.1.1"
seq-queue@^0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e"
@ -13556,6 +13904,13 @@ smart-buffer@^4.2.0:
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
snake-case@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-1.1.2.tgz#0c2f25e305158d9a18d3d977066187fef8a5a66a"
integrity sha1-DC8l4wUVjZoY09l3BmGH/vilpmo=
dependencies:
sentence-case "^1.1.2"
snapdragon-node@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
@ -14027,6 +14382,23 @@ stylis@4.0.13:
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91"
integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==
superagent@^5.1.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/superagent/-/superagent-5.3.1.tgz#d62f3234d76b8138c1320e90fa83dc1850ccabf1"
integrity sha512-wjJ/MoTid2/RuGCOFtlacyGNxN9QLMgcpYLDQlWFIhhdJ93kNscFonGvrpAHSCVjRVj++DGCglocF7Aej1KHvQ==
dependencies:
component-emitter "^1.3.0"
cookiejar "^2.1.2"
debug "^4.1.1"
fast-safe-stringify "^2.0.7"
form-data "^3.0.0"
formidable "^1.2.2"
methods "^1.1.2"
mime "^2.4.6"
qs "^6.9.4"
readable-stream "^3.6.0"
semver "^7.3.2"
superjson@1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/superjson/-/superjson-1.8.1.tgz#c0fe510b8ff71c3bde5733a125623994ca9ec608"
@ -14086,6 +14458,26 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
svix-fetch@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/svix-fetch/-/svix-fetch-3.0.0.tgz#c13e20b69ceb3ad43b52dd3933ec30bf278c571d"
integrity sha512-rcADxEFhSqHbraZIsjyZNh4TF6V+koloX1OzZ+AQuObX9mZ2LIMhm1buZeuc5BIZPftZpJCMBsSiBaeszo9tRw==
dependencies:
node-fetch "^2.6.1"
whatwg-fetch "^3.4.1"
svix@0.42.3:
version "0.42.3"
resolved "https://registry.yarnpkg.com/svix/-/svix-0.42.3.tgz#1ee7804f54e58017ed6bf03a9231034145a7ae90"
integrity sha512-Ynao0T64OadkG5Lgd9YDvrYFAGsydXck4Id5FQQZ+n0oMXyRYzVoYzeLE/1duChQSzhc9V3FOJGLWl0d11wjLQ==
dependencies:
"@stablelib/base64" "^1.0.0"
"@stablelib/utf8" "^1.0.0"
es6-promise "^4.2.4"
fast-sha256 "^1.3.0"
svix-fetch "^3.0.0"
url-parse "^1.4.3"
swagger-client@^3.18.4:
version "3.18.4"
resolved "https://registry.yarnpkg.com/swagger-client/-/swagger-client-3.18.4.tgz#71be9df585157a3335a542c407733d2134fa75e9"
@ -14164,6 +14556,14 @@ swagger-ui-react@4.10.3:
xml-but-prettier "^1.0.1"
zenscroll "^4.0.2"
swap-case@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/swap-case/-/swap-case-1.1.2.tgz#c39203a4587385fad3c850a0bd1bcafa081974e3"
integrity sha1-w5IDpFhzhfrTyFCgvRvK+ggZdOM=
dependencies:
lower-case "^1.1.1"
upper-case "^1.1.1"
swarm-js@^0.1.40:
version "0.1.40"
resolved "https://registry.yarnpkg.com/swarm-js/-/swarm-js-0.1.40.tgz#b1bc7b6dcc76061f6c772203e004c11997e06b99"
@ -14370,6 +14770,14 @@ tinygradient@^1.1.5:
"@types/tinycolor2" "^1.4.0"
tinycolor2 "^1.0.0"
title-case@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/title-case/-/title-case-1.1.2.tgz#fae4a6ae546bfa22d083a0eea910a40d12ed4f5a"
integrity sha1-+uSmrlRr+iLQg6DuqRCkDRLtT1o=
dependencies:
sentence-case "^1.1.1"
upper-case "^1.0.3"
title@^3.4.2:
version "3.4.4"
resolved "https://registry.yarnpkg.com/title/-/title-3.4.4.tgz#5c0ab11fd69643bc05dc006bba52aaf5c1630f5e"
@ -15024,6 +15432,18 @@ update-input-width@^1.2.2:
resolved "https://registry.yarnpkg.com/update-input-width/-/update-input-width-1.2.2.tgz#9a6a35858ae8e66fbfe0304437b23a4934fc7d37"
integrity sha512-6QwD9ZVSXb96PxOZ01DU0DJTPwQGY7qBYgdniZKJN02Xzom2m+9J6EPxMbefskqtj4x78qbe5psDSALq9iNEYg==
upper-case-first@^1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-1.1.2.tgz#5d79bedcff14419518fd2edb0a0507c9b6859115"
integrity sha1-XXm+3P8UQZUY/S7bCgUHybaFkRU=
dependencies:
upper-case "^1.1.1"
upper-case@^1.0.3, upper-case@^1.1.0, upper-case@^1.1.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598"
integrity sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=
uri-js@^4.2.2:
version "4.4.1"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
@ -15649,6 +16069,11 @@ whatwg-encoding@^1.0.5:
dependencies:
iconv-lite "0.4.24"
whatwg-fetch@^3.4.1:
version "3.6.2"
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c"
integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==
whatwg-mimetype@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
@ -15935,7 +16360,7 @@ yallist@4.0.0, yallist@^4.0.0:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yallist@^2.1.2:
yallist@^2.0.0, yallist@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=