From 9df4867fca26992d22cb0189cc485a8f910a700a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20L=C3=B3pez?= Date: Thu, 26 May 2022 11:07:14 -0600 Subject: [PATCH] License server (#2379) * WIP License server * WIP * Moves locations to App Store and Core * LocationType fixes * Runs db migrations post-deploy * WIP * WIP * Cleanup * WIP * WIP * Decouples translations from NavTabs * Adds admin submodule * Adds admin submodule * Sync dependencies * WIP * WIP * Updates submodules * Renames package * Updates submodules * Adds scripts for console * Updates license checker URL * Updates admin * Adds staging/prod admin console links * Update yarn.lock * Update NavTabs.tsx * WIP * Update admin * WIP * Adds hint to InputField * Update admin * Adds turbo admin dependecies * Update admin * Prevents redirection on form submit * Form warning fixes * Update admin * Form fixes * Update yarn.lock * Update admin * Update admin * Update admin * Adds withLicenseRequired HOC * Adds LicenseRequired to EE components * Admin deploy fix? * Updates submodules * Use relative inside lib * type fixes * Fixes turbo race condition * Relocates admin to console * Relocates admin to console * Update console * Update api * Update turbo.json * Update ErrorBoundary.tsx * Update defaultEvents.ts * Update checkLicense.ts * Update yarn.lock * Skip on E2E Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .env.example | 3 + apps/api | 2 +- apps/console | 2 +- apps/web/components/AppsShell.tsx | 24 ++-- apps/web/components/BookingsShell.tsx | 41 +++--- apps/web/components/EmptyScreen.tsx | 10 +- apps/web/components/NavTabs.tsx | 5 +- apps/web/components/SettingsShell.tsx | 78 ++++++----- apps/web/components/Shell.tsx | 3 +- apps/web/components/ui/ModalContainer.tsx | 2 +- apps/web/ee/components/LicenseRequired.tsx | 57 ++++++++ .../components/apiKeys/ApiKeyDialogForm.tsx | 13 +- .../apiKeys/ApiKeyListContainer.tsx | 118 +++++++++-------- apps/web/ee/components/saml/Configuration.tsx | 10 +- apps/web/ee/components/stripe/PaymentPage.tsx | 6 +- .../availability/TeamAvailabilityModal.tsx | 113 ++++++++-------- apps/web/ee/components/web3/CryptoSection.tsx | 3 +- .../settings/teams/[id]/availability.tsx | 25 ++-- apps/web/lib/ErrorBoundary.tsx | 35 +++++ apps/web/lib/app-providers.tsx | 3 +- apps/web/package.json | 2 + apps/web/pages/_app.tsx | 9 +- apps/web/pages/api/auth/[...nextauth].tsx | 3 + apps/web/pages/settings/billing.tsx | 11 +- apps/web/pages/settings/profile.tsx | 11 +- apps/web/pages/settings/security.tsx | 9 +- apps/web/pages/settings/teams/index.tsx | 17 ++- package.json | 7 +- packages/ee/server/checkLicense.ts | 25 ++++ packages/lib/availability.ts | 3 +- packages/lib/defaultEvents.ts | 3 +- packages/lib/isPrismaObj.ts | 2 +- packages/lib/package.json | 2 + packages/types/environment.d.ts | 2 + packages/types/next-auth.d.ts | 1 + turbo.json | 3 - yarn.lock | 124 +++++++++++++----- 37 files changed, 502 insertions(+), 285 deletions(-) create mode 100644 apps/web/ee/components/LicenseRequired.tsx create mode 100644 apps/web/lib/ErrorBoundary.tsx create mode 100644 packages/ee/server/checkLicense.ts diff --git a/.env.example b/.env.example index 72a2e0f769..a2d142392f 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,8 @@ # - You can not repackage or sell the codebase # - Acquire a commercial license to remove these terms by visiting: cal.com/sales NEXT_PUBLIC_LICENSE_CONSENT='' +# To enable enterprise-only features, fill your license key in here +CALCOM_LICENSE_KEY= # *********************************************************************************************************** # - DATABASE ************************************************************************************************ @@ -25,6 +27,7 @@ NEXT_PUBLIC_LICENSE_CONSENT='' NEXT_PUBLIC_WEBAPP_URL='http://localhost:3000' # Change to 'http://localhost:3001' if running the website simultaneously NEXT_PUBLIC_WEBSITE_URL='http://localhost:3000' +NEXT_PUBLIC_CONSOLE_URL='http://localhost:3004' NEXT_PUBLIC_EMBED_LIB_URL='http://localhost:3000/embed/embed.js' # To enable SAML login, set both these variables diff --git a/apps/api b/apps/api index 14aca7ef2b..76b3f1ba3a 160000 --- a/apps/api +++ b/apps/api @@ -1 +1 @@ -Subproject commit 14aca7ef2b81421bdf4c95020bf54255abe34dcb +Subproject commit 76b3f1ba3a6a7aad37b32ce4afee3c26420b18f4 diff --git a/apps/console b/apps/console index dfa050650c..3c6d61a703 160000 --- a/apps/console +++ b/apps/console @@ -1 +1 @@ -Subproject commit dfa050650c1507f24ec81d972c0c697ec07934d9 +Subproject commit 3c6d61a703f1b1ce8739f0fe86fcd78b67085a22 diff --git a/apps/web/components/AppsShell.tsx b/apps/web/components/AppsShell.tsx index 51f55c7448..23755ab80d 100644 --- a/apps/web/components/AppsShell.tsx +++ b/apps/web/components/AppsShell.tsx @@ -1,23 +1,21 @@ import { useSession } from "next-auth/react"; import React from "react"; -import { useLocale } from "@calcom/lib/hooks/useLocale"; - import NavTabs from "./NavTabs"; +const tabs = [ + { + name: "app_store", + href: "/apps", + }, + { + name: "installed_apps", + href: "/apps/installed", + }, +]; + export default function AppsShell({ children }: { children: React.ReactNode }) { - const { t } = useLocale(); const { status } = useSession(); - const tabs = [ - { - name: t("app_store"), - href: "/apps", - }, - { - name: t("installed_apps"), - href: "/apps/installed", - }, - ]; return ( <> diff --git a/apps/web/components/BookingsShell.tsx b/apps/web/components/BookingsShell.tsx index 014e379eca..bbf5894dbb 100644 --- a/apps/web/components/BookingsShell.tsx +++ b/apps/web/components/BookingsShell.tsx @@ -1,30 +1,27 @@ import React from "react"; -import { useLocale } from "@lib/hooks/useLocale"; - import NavTabs from "./NavTabs"; -export default function BookingsShell({ children }: { children: React.ReactNode }) { - const { t } = useLocale(); - const tabs = [ - { - name: t("upcoming"), - href: "/bookings/upcoming", - }, - { - name: t("recurring"), - href: "/bookings/recurring", - }, - { - name: t("past"), - href: "/bookings/past", - }, - { - name: t("cancelled"), - href: "/bookings/cancelled", - }, - ]; +const tabs = [ + { + name: "upcoming", + href: "/bookings/upcoming", + }, + { + name: "recurring", + href: "/bookings/recurring", + }, + { + name: "past", + href: "/bookings/past", + }, + { + name: "cancelled", + href: "/bookings/cancelled", + }, +]; +export default function BookingsShell({ children }: { children: React.ReactNode }) { return ( <> diff --git a/apps/web/components/EmptyScreen.tsx b/apps/web/components/EmptyScreen.tsx index 4118800cbe..d8ba88fea6 100644 --- a/apps/web/components/EmptyScreen.tsx +++ b/apps/web/components/EmptyScreen.tsx @@ -9,17 +9,17 @@ export default function EmptyScreen({ }: { Icon: SVGComponent; headline: string; - description: string; + description: string | React.ReactElement; }) { return ( <>
-
- +
+
-

{headline}

-

{description}

+

{headline}

+

{description}

diff --git a/apps/web/components/NavTabs.tsx b/apps/web/components/NavTabs.tsx index bc3de08adb..fa42293f46 100644 --- a/apps/web/components/NavTabs.tsx +++ b/apps/web/components/NavTabs.tsx @@ -4,6 +4,8 @@ import Link, { LinkProps } from "next/link"; import { useRouter } from "next/router"; import { FC, Fragment, MouseEventHandler } from "react"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; + import classNames from "@lib/classNames"; import { SVGComponent } from "@lib/types/SVGComponent"; @@ -22,6 +24,7 @@ export interface NavTabProps { const NavTabs: FC = ({ tabs, linkProps, ...props }) => { const router = useRouter(); + const { t } = useLocale(); return ( <>
{/* show bottom navigation for md and smaller (tablet and phones) */} {status === "authenticated" && ( diff --git a/apps/web/components/ui/ModalContainer.tsx b/apps/web/components/ui/ModalContainer.tsx index 6396420757..5fa6353350 100644 --- a/apps/web/components/ui/ModalContainer.tsx +++ b/apps/web/components/ui/ModalContainer.tsx @@ -18,7 +18,7 @@ export default function ModalContainer(props: Props) {
= ({ children, as = "", ...rest }) => { + const session = useSession(); + const Component = as || Fragment; + return ( + + {session.data?.hasValidLicense ? ( + children + ) : ( + + To enable this feature, get a deployment key at{" "} + + Cal.com console + + . + + } + /> + )} + + ); +}; + +export function withLicenseRequired(Component: ComponentType) { + // eslint-disable-next-line react/display-name + return (hocProps: T) => { + return ( + + ; + + ); + }; +} + +export default LicenseRequired; diff --git a/apps/web/ee/components/apiKeys/ApiKeyDialogForm.tsx b/apps/web/ee/components/apiKeys/ApiKeyDialogForm.tsx index 99730950cc..783036f464 100644 --- a/apps/web/ee/components/apiKeys/ApiKeyDialogForm.tsx +++ b/apps/web/ee/components/apiKeys/ApiKeyDialogForm.tsx @@ -15,11 +15,12 @@ import { trpc } from "@lib/trpc"; import { DatePicker } from "@components/ui/form/DatePicker"; -import { TApiKeys } from "./ApiKeyListItem"; +import LicenseRequired from "../LicenseRequired"; +import type { TApiKeys } from "./ApiKeyListItem"; export default function ApiKeyDialogForm(props: { title: string; - defaultValues?: Omit & { neverExpires: boolean }; + defaultValues?: Omit & { neverExpires?: boolean }; handleClose: () => void; }) { const { t } = useLocale(); @@ -49,7 +50,7 @@ export default function ApiKeyDialogForm(props: { const watchNeverExpires = form.watch("neverExpires"); return ( - <> + {successfulNewApiKeyModal ? ( <>
@@ -92,12 +93,12 @@ export default function ApiKeyDialogForm(props: { ) : ( - & { neverExpires: boolean }> + & { neverExpires?: boolean }> form={form} handleSubmit={async (event) => { const apiKey = await utils.client.mutation("viewer.apiKeys.create", event); setApiKey(apiKey); - setApiKeyDetails({ ...event }); + setApiKeyDetails({ ...event, neverExpires: !!event.neverExpires }); await utils.invalidateQueries(["viewer.apiKeys.list"]); setSuccessfulNewApiKeyModal(true); }} @@ -146,6 +147,6 @@ export default function ApiKeyDialogForm(props: { )} - + ); } diff --git a/apps/web/ee/components/apiKeys/ApiKeyListContainer.tsx b/apps/web/ee/components/apiKeys/ApiKeyListContainer.tsx index 5e71b13c84..0bc28d54d1 100644 --- a/apps/web/ee/components/apiKeys/ApiKeyListContainer.tsx +++ b/apps/web/ee/components/apiKeys/ApiKeyListContainer.tsx @@ -12,66 +12,76 @@ import { trpc } from "@lib/trpc"; import { List } from "@components/List"; -export default function ApiKeyListContainer() { +import LicenseRequired from "../LicenseRequired"; + +function ApiKeyListContainer() { const { t } = useLocale(); const query = trpc.useQuery(["viewer.apiKeys.list"]); const [newApiKeyModal, setNewApiKeyModal] = useState(false); const [editModalOpen, setEditModalOpen] = useState(false); - const [apiKeyToEdit, setApiKeyToEdit] = useState<(TApiKeys & { neverExpires: boolean }) | null>(null); + const [apiKeyToEdit, setApiKeyToEdit] = useState<(TApiKeys & { neverExpires?: boolean }) | null>(null); return ( - ( - <> -
-
-

{t("api_keys")}

-

{t("api_keys_subtitle")}

-
-
- -
-
- - {data.length > 0 && ( - - {data.map((item: any) => ( - { - setApiKeyToEdit(item); - setEditModalOpen(true); - }} - /> - ))} - - )} - - {/* New api key dialog */} - !isOpen && setNewApiKeyModal(false)}> - - setNewApiKeyModal(false)} /> - - - {/* Edit api key dialog */} - !isOpen && setEditModalOpen(false)}> - - {apiKeyToEdit && ( - setEditModalOpen(false)} - defaultValues={apiKeyToEdit} - /> + <> +
+
+

{t("api_keys")}

+

{t("api_keys_subtitle")}

+
+
+ +
+
+ + ( + <> + {data.length > 0 && ( + + {data.map((item) => ( + { + setApiKeyToEdit(item); + setEditModalOpen(true); + }} + /> + ))} + )} -
-
- - )} - /> + + {/* New api key dialog */} + !isOpen && setNewApiKeyModal(false)}> + + setNewApiKeyModal(false)} + /> + + + {/* Edit api key dialog */} + !isOpen && setEditModalOpen(false)}> + + {apiKeyToEdit && ( + setEditModalOpen(false)} + defaultValues={apiKeyToEdit} + /> + )} + + + + )} + /> + + ); } + +export default ApiKeyListContainer; diff --git a/apps/web/ee/components/saml/Configuration.tsx b/apps/web/ee/components/saml/Configuration.tsx index 4b32f7ab51..fa4a632d7e 100644 --- a/apps/web/ee/components/saml/Configuration.tsx +++ b/apps/web/ee/components/saml/Configuration.tsx @@ -1,18 +1,20 @@ -import React, { useEffect, useState, useRef } from "react"; +import React, { useEffect, useRef, useState } from "react"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; import showToast from "@calcom/lib/notification"; import { Alert } from "@calcom/ui/Alert"; import Button from "@calcom/ui/Button"; import { Dialog, DialogTrigger } from "@calcom/ui/Dialog"; import { TextArea } from "@calcom/ui/form/fields"; -import { useLocale } from "@lib/hooks/useLocale"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry"; import { trpc } from "@lib/trpc"; import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent"; import Badge from "@components/ui/Badge"; +import LicenseRequired from "../LicenseRequired"; + export default function SAMLConfiguration({ teamsView, teamId, @@ -92,7 +94,7 @@ export default function SAMLConfiguration({ return ( <> {isSAMLLoginEnabled ? ( - <> +

@@ -157,7 +159,7 @@ export default function SAMLConfiguration({


- +
) : null} ); diff --git a/apps/web/ee/components/stripe/PaymentPage.tsx b/apps/web/ee/components/stripe/PaymentPage.tsx index b68fdbbc50..76ca06ee1e 100644 --- a/apps/web/ee/components/stripe/PaymentPage.tsx +++ b/apps/web/ee/components/stripe/PaymentPage.tsx @@ -11,6 +11,7 @@ import { FormattedNumber, IntlProvider } from "react-intl"; import { sdkActionManager, useIsEmbed } from "@calcom/embed-core"; import getStripe from "@calcom/stripe/client"; +import LicenseRequired from "@ee/components/LicenseRequired"; import PaymentComponent from "@ee/components/stripe/Payment"; import { PaymentPageProps } from "@ee/pages/payment/[uid]"; @@ -70,7 +71,8 @@ const PaymentPage: FC = (props) => { - )} -
+
diff --git a/apps/web/ee/components/team/availability/TeamAvailabilityModal.tsx b/apps/web/ee/components/team/availability/TeamAvailabilityModal.tsx index 43e5640d8b..583f196e78 100644 --- a/apps/web/ee/components/team/availability/TeamAvailabilityModal.tsx +++ b/apps/web/ee/components/team/availability/TeamAvailabilityModal.tsx @@ -3,8 +3,9 @@ import utc from "dayjs/plugin/utc"; import React, { useState, useEffect } from "react"; import { WEBSITE_URL } from "@calcom/lib/constants"; +import LicenseRequired from "@ee/components/LicenseRequired"; -import { trpc, inferQueryOutput } from "@lib/trpc"; +import { inferQueryOutput, trpc } from "@lib/trpc"; import Avatar from "@components/ui/Avatar"; import { DatePicker } from "@components/ui/form/DatePicker"; @@ -33,62 +34,66 @@ export default function TeamAvailabilityModal(props: Props) { }, [utils, selectedTimeZone, selectedDate]); return ( -
-
-
- -
- {props.member?.name} - {props.member?.email} + +
+
+
+ +
+ {props.member?.name} + {props.member?.email} +
+
+
+ Date + { + setSelectedDate(dayjs(newDate)); + }} + /> +
+
+ Timezone + setSelectedTimeZone(timezone.value)} + classNamePrefix="react-select" + className="react-select-container mt-1 block w-full rounded-sm border border-gray-300 shadow-sm focus:border-neutral-800 focus:ring-neutral-800 sm:text-sm" + /> +
+
+ Slot Length + setFrequency(newFrequency?.value ?? 30)} - /> -
+ )}
- {props.team && props.member && ( - - )} -
+
); } diff --git a/apps/web/ee/components/web3/CryptoSection.tsx b/apps/web/ee/components/web3/CryptoSection.tsx index 4d570429ee..0056c65f85 100644 --- a/apps/web/ee/components/web3/CryptoSection.tsx +++ b/apps/web/ee/components/web3/CryptoSection.tsx @@ -11,6 +11,7 @@ import { Button } from "@calcom/ui/Button"; import { useContracts } from "../../../contexts/contractsContext"; import genericAbi from "../../../web3/abis/abiWithGetBalance.json"; import verifyAccount, { AUTH_MESSAGE } from "../../../web3/utils/verifyAccount"; +import { withLicenseRequired } from "../LicenseRequired"; interface Window { ethereum: AbstractProvider & { selectedAddress: string }; @@ -150,4 +151,4 @@ const CryptoSection = (props: CryptoSectionProps) => { ); }; -export default CryptoSection; +export default withLicenseRequired(CryptoSection); diff --git a/apps/web/ee/pages/settings/teams/[id]/availability.tsx b/apps/web/ee/pages/settings/teams/[id]/availability.tsx index 49cba9598c..d920cfb6aa 100644 --- a/apps/web/ee/pages/settings/teams/[id]/availability.tsx +++ b/apps/web/ee/pages/settings/teams/[id]/availability.tsx @@ -2,6 +2,7 @@ import { useRouter } from "next/router"; import { useMemo, useState } from "react"; import { Alert } from "@calcom/ui/Alert"; +import LicenseRequired from "@ee/components/LicenseRequired"; import TeamAvailabilityScreen from "@ee/components/team/availability/TeamAvailabilityScreen"; import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar"; @@ -49,17 +50,19 @@ export function TeamAvailabilityPage() { /> ) }> - {!!errorMessage && } - {isLoading && } - {isFreeUser ? ( - - ) : ( - TeamAvailability - )} + + {!!errorMessage && } + {isLoading && } + {isFreeUser ? ( + + ) : ( + TeamAvailability + )} + ); } diff --git a/apps/web/lib/ErrorBoundary.tsx b/apps/web/lib/ErrorBoundary.tsx new file mode 100644 index 0000000000..9fee8fa915 --- /dev/null +++ b/apps/web/lib/ErrorBoundary.tsx @@ -0,0 +1,35 @@ +import React, { ErrorInfo } from "react"; + +class ErrorBoundary extends React.Component< + { children: React.ReactNode }, + { error: Error | null; errorInfo: ErrorInfo | null } +> { + constructor(props: { children: React.ReactNode } | Readonly<{ children: React.ReactNode }>) { + super(props); + this.state = { error: null, errorInfo: null }; + } + + componentDidCatch?(error: Error, errorInfo: ErrorInfo) { + // Catch errors in any components below and re-render with error message + this.setState({ error, errorInfo }); + // You can also log error messages to an error reporting service here + } + + render() { + if (this.state.errorInfo) { + // Error path + return ( +
+

Something went wrong.

+
+ {this.state.error && this.state.error.toString()} +
+
+ ); + } + // Normally, just render children + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/apps/web/lib/app-providers.tsx b/apps/web/lib/app-providers.tsx index 6fb3f97717..d6f6bfa333 100644 --- a/apps/web/lib/app-providers.tsx +++ b/apps/web/lib/app-providers.tsx @@ -16,7 +16,8 @@ const I18nextAdapter = appWithTranslation & { + Component: NextAppProps["Component"] & { requiresLicense?: boolean }; /** Will be defined only is there was an error */ err?: Error; }; diff --git a/apps/web/package.json b/apps/web/package.json index 811ef7849d..1a28b1ed3a 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -76,6 +76,7 @@ "jimp": "^0.16.1", "libphonenumber-js": "^1.9.53", "lodash": "^4.17.21", + "memory-cache": "^0.2.0", "micro": "^9.3.4", "mime-types": "^2.1.35", "next": "^12.1.6", @@ -125,6 +126,7 @@ "@types/glidejs__glide": "^3.4.2", "@types/jest": "^27.5.1", "@types/lodash": "^4.14.182", + "@types/memory-cache": "^0.2.2", "@types/micro": "7.3.6", "@types/mime-types": "^2.1.1", "@types/module-alias": "^2.0.1", diff --git a/apps/web/pages/_app.tsx b/apps/web/pages/_app.tsx index 794102f2b9..3f1f1eae4e 100644 --- a/apps/web/pages/_app.tsx +++ b/apps/web/pages/_app.tsx @@ -3,6 +3,7 @@ import Head from "next/head"; import superjson from "superjson"; import "@calcom/embed-core/src/embed-iframe"; +import LicenseRequired from "@ee/components/LicenseRequired"; import AppProviders, { AppProps } from "@lib/app-providers"; import { seoConfig } from "@lib/config/next-seo.config"; @@ -37,7 +38,13 @@ function MyApp(props: AppProps) { - + {Component.requiresLicense ? ( + + + + ) : ( + + )} ); diff --git a/apps/web/pages/api/auth/[...nextauth].tsx b/apps/web/pages/api/auth/[...nextauth].tsx index ccd947bc00..f63ec6d407 100644 --- a/apps/web/pages/api/auth/[...nextauth].tsx +++ b/apps/web/pages/api/auth/[...nextauth].tsx @@ -10,6 +10,7 @@ import nodemailer, { TransportOptions } from "nodemailer"; import { authenticator } from "otplib"; import path from "path"; +import checkLicense from "@calcom/ee/server/checkLicense"; import { WEBSITE_URL } from "@calcom/lib/constants"; import { symmetricDecrypt } from "@calcom/lib/crypto"; import { defaultCookies } from "@calcom/lib/default-cookies"; @@ -276,8 +277,10 @@ export default NextAuth({ return token; }, async session({ session, token }) { + const hasValidLicense = await checkLicense(process.env.CALCOM_LICENSE_KEY || ""); const calendsoSession: Session = { ...session, + hasValidLicense, user: { ...session.user, id: token.id as number, diff --git a/apps/web/pages/settings/billing.tsx b/apps/web/pages/settings/billing.tsx index 7a22a388e6..96acb03822 100644 --- a/apps/web/pages/settings/billing.tsx +++ b/apps/web/pages/settings/billing.tsx @@ -1,14 +1,13 @@ import { ExternalLinkIcon } from "@heroicons/react/solid"; import { ReactNode } from "react"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; import Button from "@calcom/ui/Button"; import { useIntercom } from "@ee/lib/intercom/useIntercom"; -import { useLocale } from "@lib/hooks/useLocale"; import useMeQuery from "@lib/hooks/useMeQuery"; import SettingsShell from "@components/SettingsShell"; -import Shell from "@components/Shell"; type CardProps = { title: string; description: string; className?: string; children: ReactNode }; const Card = ({ title, description, className = "", children }: CardProps): JSX.Element => ( @@ -30,8 +29,8 @@ export default function Billing() { const { boot, show } = useIntercom(); return ( - - + + <>
{data?.plan && ["FREE", "TRIAL"].includes(data.plan) && (
- - + + ); } diff --git a/apps/web/pages/settings/profile.tsx b/apps/web/pages/settings/profile.tsx index a798945022..229421e441 100644 --- a/apps/web/pages/settings/profile.tsx +++ b/apps/web/pages/settings/profile.tsx @@ -5,6 +5,7 @@ import { signOut } from "next-auth/react"; import { useRouter } from "next/router"; import { ComponentProps, FormEvent, RefObject, useEffect, useMemo, useRef, useState } from "react"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; import showToast from "@calcom/lib/notification"; import { Alert } from "@calcom/ui/Alert"; import Button from "@calcom/ui/Button"; @@ -15,7 +16,6 @@ import { withQuery } from "@lib/QueryCell"; import { asStringOrNull, asStringOrUndefined } from "@lib/asStringOrNull"; import { getSession } from "@lib/auth"; import { nameOfDay } from "@lib/core/i18n/weekday"; -import { useLocale } from "@lib/hooks/useLocale"; import { isBrandingHidden } from "@lib/isBrandingHidden"; import prisma from "@lib/prisma"; import { trpc } from "@lib/trpc"; @@ -23,7 +23,6 @@ import { inferSSRProps } from "@lib/types/inferSSRProps"; import ImageUploader from "@components/ImageUploader"; import SettingsShell from "@components/SettingsShell"; -import Shell from "@components/Shell"; import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent"; import Avatar from "@components/ui/Avatar"; import Badge from "@components/ui/Badge"; @@ -488,11 +487,9 @@ export default function Settings(props: Props) { const { t } = useLocale(); return ( - - - } /> - - + + } /> + ); } diff --git a/apps/web/pages/settings/security.tsx b/apps/web/pages/settings/security.tsx index 1fda179798..a88784319a 100644 --- a/apps/web/pages/settings/security.tsx +++ b/apps/web/pages/settings/security.tsx @@ -9,7 +9,6 @@ import { identityProviderNameMap } from "@lib/auth"; import { trpc } from "@lib/trpc"; import SettingsShell from "@components/SettingsShell"; -import Shell from "@components/Shell"; import ChangePasswordSection from "@components/security/ChangePasswordSection"; import DisableUserImpersonation from "@components/security/DisableUserImpersonation"; import TwoFactorAuthSection from "@components/security/TwoFactorAuthSection"; @@ -18,8 +17,8 @@ export default function Security() { const user = trpc.useQuery(["viewer.me"]).data; const { t } = useLocale(); return ( - - + + <> {user && user.identityProvider !== IdentityProvider.CAL ? ( <>
@@ -45,7 +44,7 @@ export default function Security() { )} - - + + ); } diff --git a/apps/web/pages/settings/teams/index.tsx b/apps/web/pages/settings/teams/index.tsx index be391687cf..1ad9065c7b 100644 --- a/apps/web/pages/settings/teams/index.tsx +++ b/apps/web/pages/settings/teams/index.tsx @@ -4,21 +4,20 @@ import { useSession } from "next-auth/react"; import { Trans } from "next-i18next"; import { useState } from "react"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Alert } from "@calcom/ui/Alert"; import Button from "@calcom/ui/Button"; -import { useLocale } from "@lib/hooks/useLocale"; import useMeQuery from "@lib/hooks/useMeQuery"; import { trpc } from "@lib/trpc"; import EmptyScreen from "@components/EmptyScreen"; import Loader from "@components/Loader"; import SettingsShell from "@components/SettingsShell"; -import Shell from "@components/Shell"; import TeamCreateModal from "@components/team/TeamCreateModal"; import TeamList from "@components/team/TeamList"; -export default function Teams() { +function Teams() { const { t } = useLocale(); const { status } = useSession(); const loading = status === "loading"; @@ -40,8 +39,8 @@ export default function Teams() { const isFreePlan = me.data?.plan === "FREE"; return ( - - + + <> {!!errorMessage && } {isFreePlan && ( )} {teams.length > 0 && } - - + + ); } + +Teams.requiresLicense = false; + +export default Teams; diff --git a/package.json b/package.json index bb0d14e792..056db78c68 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,11 @@ "db-studio": "yarn workspace @calcom/prisma db-studio", "deploy": "turbo run deploy", "dev": "turbo run dev --scope=\"@calcom/web\"", - "dev:website": "yarn predev && turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/website\"", - "dev:api": "yarn predev && turbo run dev --scope=\"@calcom/api\"", - "dev:swagger": "yarn predev && turbo run dev --scope=\"@calcom/api\" --scope=\"@calcom/swagger\"", + "dev:all": "yarn predev && turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/website\" --scope=\"@calcom/console\"", + "dev:api": "yarn predev && turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/api\"", "dev:console": "yarn predev && turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/console\"", + "dev:swagger": "yarn predev && turbo run dev --scope=\"@calcom/api\" --scope=\"@calcom/swagger\"", + "dev:website": "yarn predev && turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/website\"", "docs-dev": "yarn predev && turbo run dev --scope=\"@calcom/docs\"", "docs-build": "turbo run build --scope=\"@calcom/docs\" --include-dependencies", "docs-start": "turbo run start --scope=\"@calcom/docs\"", diff --git a/packages/ee/server/checkLicense.ts b/packages/ee/server/checkLicense.ts new file mode 100644 index 0000000000..cf359912e4 --- /dev/null +++ b/packages/ee/server/checkLicense.ts @@ -0,0 +1,25 @@ +import cache from "memory-cache"; + +import { CONSOLE_URL } from "@calcom/lib/constants"; + +const CACHING_TIME = 86400000; // 24 hours in milliseconds + +async function checkLicense(license: string): Promise { + if (!!process.env.NEXT_PUBLIC_IS_E2E) return true; + const url = `${CONSOLE_URL}/api/license?key=${license}`; + const cachedResponse = cache.get(url); + if (cachedResponse) { + return cachedResponse; + } else { + try { + const response = await fetch(url); + const data = await response.json(); + cache.put(url, data.valid, CACHING_TIME); + return data.valid; + } catch (error) { + return false; + } + } +} + +export default checkLicense; diff --git a/packages/lib/availability.ts b/packages/lib/availability.ts index 4bb39609a9..a27932e573 100644 --- a/packages/lib/availability.ts +++ b/packages/lib/availability.ts @@ -4,9 +4,10 @@ import customParseFormat from "dayjs/plugin/customParseFormat"; import timezone from "dayjs/plugin/timezone"; import utc from "dayjs/plugin/utc"; -import { nameOfDay } from "@calcom/lib/weekday"; import type { Schedule, TimeRange, WorkingHours } from "@calcom/types/schedule"; +import { nameOfDay } from "./weekday"; + dayjs.extend(utc); dayjs.extend(timezone); dayjs.extend(customParseFormat); diff --git a/packages/lib/defaultEvents.ts b/packages/lib/defaultEvents.ts index 18bb1da7d2..b7e3050ad3 100644 --- a/packages/lib/defaultEvents.ts +++ b/packages/lib/defaultEvents.ts @@ -1,4 +1,5 @@ -import { PeriodType, SchedulingType, UserPlan, EventTypeCustomInput } from "@prisma/client"; +import type { EventTypeCustomInput } from "@prisma/client"; +import { PeriodType, SchedulingType, UserPlan } from "@prisma/client"; const availability = [ { diff --git a/packages/lib/isPrismaObj.ts b/packages/lib/isPrismaObj.ts index 797f4be7b4..d405ab32b3 100644 --- a/packages/lib/isPrismaObj.ts +++ b/packages/lib/isPrismaObj.ts @@ -1,4 +1,4 @@ -import { Prisma } from "@prisma/client"; +import type { Prisma } from "@prisma/client"; function isPrismaObj(obj: unknown): obj is Prisma.JsonObject { return typeof obj === "object" && !Array.isArray(obj); diff --git a/packages/lib/package.json b/packages/lib/package.json index cd3e443f45..017004bd7e 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -5,6 +5,7 @@ "types": "./index.ts", "license": "MIT", "dependencies": { + "@prisma/client": "^3.14.0", "bcryptjs": "^2.4.3", "dayjs": "^1.11.2", "ical.js": "^1.4.0", @@ -17,6 +18,7 @@ }, "devDependencies": { "@calcom/tsconfig": "*", + "@calcom/types": "*", "typescript": "^4.6.4" } } diff --git a/packages/types/environment.d.ts b/packages/types/environment.d.ts index 1156cec473..29592266df 100644 --- a/packages/types/environment.d.ts +++ b/packages/types/environment.d.ts @@ -10,6 +10,8 @@ declare namespace NodeJS { * - Acquire a commercial license to remove these terms by visiting: cal.com/sales **/ readonly NEXT_PUBLIC_LICENSE_CONSENT: "agree" | undefined; + /** Needed to enable enterprise-only features */ + readonly CALCOM_LICENSE_KEY: string | undefined; readonly CALENDSO_ENCRYPTION_KEY: string | undefined; readonly DATABASE_URL: string | undefined; readonly GOOGLE_API_CREDENTIALS: string | undefined; diff --git a/packages/types/next-auth.d.ts b/packages/types/next-auth.d.ts index bf0227f304..b447c8fb78 100644 --- a/packages/types/next-auth.d.ts +++ b/packages/types/next-auth.d.ts @@ -13,6 +13,7 @@ declare module "next-auth" { * Returned by `useSession`, `getSession` and received as a prop on the `Provider` React Context */ interface Session { + hasValidLicense: boolean; user: CalendsoSessionUser; } } diff --git a/turbo.json b/turbo.json index 4d88845e79..c9dc905c12 100644 --- a/turbo.json +++ b/turbo.json @@ -3,9 +3,6 @@ "baseBranch": "origin/main", "globalDependencies": [".env", "packages/prisma/.env"], "pipeline": { - "@calcom/admin#post-install": { - "outputs": ["./node_modules/@prisma/admin-client/**", "./apps/console/prisma/schema.prisma"] - }, "@calcom/prisma#post-install": { "outputs": ["./node_modules/@prisma/client/**", "./packages/prisma/schema.prisma"] }, diff --git a/yarn.lock b/yarn.lock index 7278ee7b64..7301a7575b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -102,20 +102,20 @@ semver "^6.3.0" "@babel/core@^7.11.6": - version "7.17.12" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.12.tgz#b4eb2d7ebc3449b062381644c93050db545b70ee" - integrity sha512-44ODe6O1IVz9s2oJE3rZ4trNNKTX9O7KpQpfAP4t8QII/zwrVRHL7i2pxhqtcY7tqMLrrKfMlBKnm1QlrRFs5w== + version "7.18.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.0.tgz#c58d04d7c6fbfb58ea7681e2b9145cfb62726756" + integrity sha512-Xyw74OlJwDijToNi0+6BBI5mLLR5+5R3bcSH80LXzjzEGEUlvNzujEE71BaD/ApEZHAvFI/Mlmp4M5lIkdeeWw== dependencies: "@ampproject/remapping" "^2.1.0" "@babel/code-frame" "^7.16.7" - "@babel/generator" "^7.17.12" + "@babel/generator" "^7.18.0" "@babel/helper-compilation-targets" "^7.17.10" - "@babel/helper-module-transforms" "^7.17.12" - "@babel/helpers" "^7.17.9" - "@babel/parser" "^7.17.12" + "@babel/helper-module-transforms" "^7.18.0" + "@babel/helpers" "^7.18.0" + "@babel/parser" "^7.18.0" "@babel/template" "^7.16.7" - "@babel/traverse" "^7.17.12" - "@babel/types" "^7.17.12" + "@babel/traverse" "^7.18.0" + "@babel/types" "^7.18.0" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" @@ -188,6 +188,15 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.18.0": + version "7.18.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.0.tgz#46d28e8a18fc737b028efb25ab105d74473af43f" + integrity sha512-81YO9gGx6voPXlvYdZBliFXAZU8vZ9AZ6z+CjlmcnaeOcYSFbMTpdeDUO9xD9dh/68Vq03I8ZspfUTPfitcDHg== + dependencies: + "@babel/types" "^7.18.0" + "@jridgewell/gen-mapping" "^0.3.0" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz#bb2339a7534a9c128e3102024c60760a3a7f3862" @@ -274,10 +283,10 @@ "@babel/traverse" "^7.17.3" "@babel/types" "^7.17.0" -"@babel/helper-module-transforms@^7.17.12": - version "7.17.12" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.17.12.tgz#bec00139520cb3feb078ef7a4578562480efb77e" - integrity sha512-t5s2BeSWIghhFRPh9XMn6EIGmvn8Lmw5RVASJzkIx1mSemubQQBNIZiQD7WzaFmaHIrjAec4x8z9Yx8SjJ1/LA== +"@babel/helper-module-transforms@^7.18.0": + version "7.18.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.0.tgz#baf05dec7a5875fb9235bd34ca18bad4e21221cd" + integrity sha512-kclUYSUBIjlvnzN2++K9f2qzYKFgjmnmjwL4zlmU5f8ZtzgWe8s0rUPSTGy2HmK4P8T52MQsS+HTQAgZd3dMEA== dependencies: "@babel/helper-environment-visitor" "^7.16.7" "@babel/helper-module-imports" "^7.16.7" @@ -285,8 +294,8 @@ "@babel/helper-split-export-declaration" "^7.16.7" "@babel/helper-validator-identifier" "^7.16.7" "@babel/template" "^7.16.7" - "@babel/traverse" "^7.17.12" - "@babel/types" "^7.17.12" + "@babel/traverse" "^7.18.0" + "@babel/types" "^7.18.0" "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.8.0": version "7.16.7" @@ -340,6 +349,15 @@ "@babel/traverse" "^7.17.9" "@babel/types" "^7.17.0" +"@babel/helpers@^7.18.0": + version "7.18.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.0.tgz#aff37c3590de42102b54842446146d0205946370" + integrity sha512-AE+HMYhmlMIbho9nbvicHyxFwhrO+xhKB6AhRxzl8w46Yj0VXTZjEsAoBVC7rB2I0jzX+yWyVybnO08qkfx6kg== + dependencies: + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.18.0" + "@babel/types" "^7.18.0" + "@babel/highlight@^7.16.7": version "7.16.10" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.10.tgz#744f2eb81579d6eea753c227b0f570ad785aba88" @@ -374,6 +392,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.9.tgz#9c94189a6062f0291418ca021077983058e171ef" integrity sha512-vqUSBLP8dQHFPdPi9bc5GK9vRkYHJ49fsZdtoJ8EQ8ibpwk5rPKfvNIwChB0KVXcIjcepEBBd2VHC5r9Gy8ueg== +"@babel/parser@^7.18.0": + version "7.18.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.0.tgz#10a8d4e656bc01128d299a787aa006ce1a91e112" + integrity sha512-AqDccGC+m5O/iUStSJy3DGRIUFu7WbY/CppZYwrEUB4N0tZlnI8CSTsgL7v5fHVFmUbRv2sd+yy27o8Ydt4MGg== + "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" @@ -590,22 +613,6 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/traverse@^7.17.12", "@babel/traverse@^7.7.2": - version "7.17.12" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.12.tgz#011874d2abbca0ccf1adbe38f6f7a4ff1747599c" - integrity sha512-zULPs+TbCvOkIFd4FrG53xrpxvCBwLIgo6tO0tJorY7YV2IWFxUfS/lXDJbGgfyYt9ery/Gxj2niwttNnB0gIw== - dependencies: - "@babel/code-frame" "^7.16.7" - "@babel/generator" "^7.17.12" - "@babel/helper-environment-visitor" "^7.16.7" - "@babel/helper-function-name" "^7.17.9" - "@babel/helper-hoist-variables" "^7.16.7" - "@babel/helper-split-export-declaration" "^7.16.7" - "@babel/parser" "^7.17.12" - "@babel/types" "^7.17.12" - debug "^4.1.0" - globals "^11.1.0" - "@babel/traverse@^7.17.9": version "7.17.9" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.9.tgz#1f9b207435d9ae4a8ed6998b2b82300d83c37a0d" @@ -622,6 +629,38 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.18.0": + version "7.18.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.0.tgz#0e5ec6db098660b2372dd63d096bf484e32d27ba" + integrity sha512-oNOO4vaoIQoGjDQ84LgtF/IAlxlyqL4TUuoQ7xLkQETFaHkY1F7yazhB4Kt3VcZGL0ZF/jhrEpnXqUb0M7V3sw== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.18.0" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.17.9" + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/parser" "^7.18.0" + "@babel/types" "^7.18.0" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/traverse@^7.7.2": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.12.tgz#011874d2abbca0ccf1adbe38f6f7a4ff1747599c" + integrity sha512-zULPs+TbCvOkIFd4FrG53xrpxvCBwLIgo6tO0tJorY7YV2IWFxUfS/lXDJbGgfyYt9ery/Gxj2niwttNnB0gIw== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.17.12" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.17.9" + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/parser" "^7.17.12" + "@babel/types" "^7.17.12" + debug "^4.1.0" + globals "^11.1.0" + "@babel/types@7.13.0": version "7.13.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.0.tgz#74424d2816f0171b4100f0ab34e9a374efdf7f80" @@ -655,6 +694,14 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" +"@babel/types@^7.18.0": + version "7.18.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.0.tgz#ef523ea349722849cb4bf806e9342ede4d071553" + integrity sha512-vhAmLPAiC8j9K2GnsnLPCIH5wCrPpYIVBCWRBFDCB7Y/BXLqi/O+1RSTTM2bsmg6U/551+FCf9PNPxjABmxHTw== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -3454,6 +3501,11 @@ resolved "https://registry.yarnpkg.com/@types/mdx/-/mdx-2.0.1.tgz#e4c05d355d092d7b58db1abfe460e53f41102ac8" integrity sha512-JPEv4iAl0I+o7g8yVWDwk30es8mfVrjkvh5UeVR2sYPpZCK44vrAPsbJpIS+rJAUxLgaSAMKTEH5Vn5qd9XsrQ== +"@types/memory-cache@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@types/memory-cache/-/memory-cache-0.2.2.tgz#f8fb6d8aa0eb006ed44fc659bf8bfdc1a5cc57fa" + integrity sha512-xNnm6EkmYYhTnLiOHC2bdKgcYY5qjjrq5vl9KXD2nh0em0koZoFS500EL4Q4V/eW+A3P7NC7P7GIYzNOSQp7jQ== + "@types/micro@7.3.6": version "7.3.6" resolved "https://registry.yarnpkg.com/@types/micro/-/micro-7.3.6.tgz#7d68eb5a780ac4761e3b80687b4ee7328ebc3f2e" @@ -10366,11 +10418,6 @@ libphonenumber-js@^1.9.52, libphonenumber-js@^1.9.53: resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.9.53.tgz#f4f3321f8fb0ee62952c2a8df4711236d2626088" integrity sha512-3cuMrA2CY3TbKVC0wKye5dXYgxmVVi4g13gzotprQSguFHMqf0pIrMM2Z6ZtMsSWqvtIqi5TuQhGjMhxz0O9Mw== -libphonenumber-js@^1.9.53: - version "1.9.53" - resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.9.53.tgz#f4f3321f8fb0ee62952c2a8df4711236d2626088" - integrity sha512-3cuMrA2CY3TbKVC0wKye5dXYgxmVVi4g13gzotprQSguFHMqf0pIrMM2Z6ZtMsSWqvtIqi5TuQhGjMhxz0O9Mw== - lie@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" @@ -10993,6 +11040,11 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== +memory-cache@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/memory-cache/-/memory-cache-0.2.0.tgz#7890b01d52c00c8ebc9d533e1f8eb17e3034871a" + integrity sha1-eJCwHVLADI68nVM+H46xfjA0hxo= + memory-pager@^1.0.2: version "1.5.0" resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5"