Update auth flow and added localisation

This commit is contained in:
SwiftyOS
2024-10-23 12:48:03 +02:00
parent 334b4d5ef9
commit 52119eadc2
49 changed files with 243 additions and 89 deletions

View File

@@ -3,6 +3,11 @@ NEXT_PUBLIC_AGPT_SERVER_URL=http://localhost:8006/api
NEXT_PUBLIC_AGPT_WS_SERVER_URL=ws://localhost:8001/ws
NEXT_PUBLIC_AGPT_MARKETPLACE_URL=http://localhost:8015/api/v1/market
## Locale settings
NEXT_PUBLIC_DEFAULT_LOCALE=en
NEXT_PUBLIC_LOCALES=en,es
## Supabase credentials
NEXT_PUBLIC_SUPABASE_URL=http://localhost:8000

View File

@@ -14,15 +14,15 @@ const nextConfig = {
images: {
domains: ["images.unsplash.com"],
},
async redirects() {
return [
{
source: "/monitor", // FIXME: Remove after 2024-09-01
destination: "/",
permanent: false,
},
];
},
// async redirects() {
// return [
// {
// source: "/monitor", // FIXME: Remove after 2024-09-01
// destination: "/",
// permanent: false,
// },
// ];
// },
// TODO: Re-enable TypeScript checks once current issues are resolved
typescript: {
ignoreBuildErrors: true,

View File

@@ -22,6 +22,7 @@
"defaults"
],
"dependencies": {
"@formatjs/intl-localematcher": "^0.5.5",
"@hookform/resolvers": "^3.9.0",
"@next/third-parties": "^14.2.5",
"@radix-ui/react-avatar": "^1.1.0",
@@ -56,6 +57,7 @@
"framer-motion": "^11.11.9",
"lucide-react": "^0.407.0",
"moment": "^2.30.1",
"negotiator": "^1.0.0",
"next": "^14.2.13",
"next-themes": "^0.3.0",
"react": "^18",
@@ -84,6 +86,7 @@
"@storybook/react": "^8.3.5",
"@storybook/test": "^8.3.5",
"@storybook/test-runner": "^0.19.1",
"@types/negotiator": "^0.6.3",
"@types/node": "^22.7.3",
"@types/react": "^18",
"@types/react-dom": "^18",

View File

@@ -0,0 +1,9 @@
import "server-only";
const dictionaries: Record<string, () => Promise<any>> = {
en: () => import("./dictionaries/en.json").then((module) => module.default),
es: () => import("./dictionaries/es.json").then((module) => module.default),
};
export const getDictionary = async (locale: string): Promise<any> =>
dictionaries[locale]();

View File

@@ -0,0 +1,22 @@
{
"auth": {
"signIn": "Sign In",
"email": "Email",
"password": "Password",
"submit": "Submit",
"error": "Invalid login credentials"
},
"dashboard": {
"welcome": "Welcome to your dashboard",
"stats": "Your Stats",
"recentActivity": "Recent Activity"
},
"admin": {
"title": "Admin Dashboard",
"users": "Users Management",
"settings": "System Settings"
},
"home": {
"welcome": "Welcome to the Home Page"
}
}

View File

@@ -0,0 +1,22 @@
{
"auth": {
"signIn": "Iniciar Sesión",
"email": "Correo electrónico",
"password": "Contraseña",
"submit": "Enviar",
"error": "Credenciales inválidas"
},
"dashboard": {
"welcome": "Bienvenido a tu panel",
"stats": "Tus Estadísticas",
"recentActivity": "Actividad Reciente"
},
"admin": {
"title": "Panel de Administración",
"users": "Gestión de Usuarios",
"settings": "Configuración del Sistema"
},
"home": {
"welcome": "Bienvenido a la Página de Inicio"
}
}

View File

@@ -0,0 +1,10 @@
import { getDictionary } from "./dictionaries";
export default async function Page({
params: { lang },
}: {
params: { lang: string };
}) {
const dict = await getDictionary(lang); // en
return <h1>{dict.home.welcome}</h1>; // Add to Cart
}

View File

@@ -1,7 +1,7 @@
import * as React from "react";
import { Button } from "./Button";
import Link from "next/link";
import { StarRatingIcons } from "../ui/icons";
import { StarRatingIcons } from "@/components/ui/icons";
interface AgentInfoProps {
onRunAgent: () => void;
name: string;

View File

@@ -2,8 +2,8 @@
import * as React from "react";
import Image from "next/image";
import { Button } from "./Button";
import { IconStarFilled, IconEdit } from "../ui/icons";
import { Card } from "../ui/card";
import { IconStarFilled, IconEdit } from "@/components/ui/icons";
import { Card } from "@/components/ui/card";
import { AgentTableRowProps } from "./AgentTableRow";
export const AgentTableCard: React.FC<AgentTableRowProps> = ({

View File

@@ -1,7 +1,7 @@
import * as React from "react";
import Image from "next/image";
import { Button } from "./Button";
import { IconStarFilled, IconEdit } from "../ui/icons";
import { IconStarFilled, IconEdit } from "@/components/ui/icons";
export interface AgentTableRowProps {
agentName: string;

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import Image from "next/image";
import { StarRatingIcons } from "../ui/icons";
import { StarRatingIcons } from "@/components/ui/icons";
interface FeaturedStoreCardProps {
agentName: string;
agentImage: string;

View File

@@ -1,7 +1,7 @@
import * as React from "react";
import Link from "next/link";
import { ProfilePopoutMenu } from "./ProfilePopoutMenu";
import { IconType, IconLogIn } from "../ui/icons";
import { IconType, IconLogIn } from "@/components/ui/icons";
import { MobileNavBar } from "./MobileNavBar";
import { Button } from "./Button";
interface NavLink {

View File

@@ -1,7 +1,7 @@
import * as React from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import Image from "next/image";
import { StarRatingIcons } from "../ui/icons";
import { StarRatingIcons } from "@/components/ui/icons";
interface StoreCardProps {
agentName: string;
agentImage: string;

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import { StoreCard } from "../StoreCard";
import { StoreCard } from "@/components/agptui/StoreCard";
import {
Carousel,
CarouselContent,

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { Avatar, AvatarImage, AvatarFallback } from "../../ui/avatar";
import { getIconForSocial, StarRatingIcons } from "../../ui/icons";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { getIconForSocial, StarRatingIcons } from "@/components/ui/icons";
interface CreatorDetailsProps {
name: string;

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import { CreatorCard } from "../CreatorCard";
import { CreatorCard } from "@/components/agptui/CreatorCard";
import {
Carousel,
CarouselContent,

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import { FeaturedStoreCard } from "../FeaturedStoreCard";
import { FeaturedStoreCard } from "@/components/agptui/FeaturedStoreCard";
interface FeaturedAgent {
agentName: string;

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { SearchBar } from "../SearchBar";
import { FilterChips } from "../FilterChips";
import { SearchBar } from "@/components/agptui/SearchBar";
import { FilterChips } from "@/components/agptui/FilterChips";
interface HeroSectionProps {
onSearch: (query: string) => void;

View File

@@ -15,6 +15,7 @@ const meta = {
},
tags: ["autodocs"],
argTypes: {
isLoggedIn: { control: "boolean" },
userName: { control: "text" },
userEmail: { control: "text" },
navLinks: { control: "object" },
@@ -155,6 +156,7 @@ const mockSimilarAgents = [
export const Default: Story = {
args: {
isLoggedIn: true,
userName: "John Doe",
userEmail: "john.doe@example.com",
navLinks: mockNavLinks,

View File

@@ -1,14 +1,15 @@
import * as React from "react";
import { Navbar } from "../Navbar";
import { AgentInfo } from "../AgentInfo";
import { AgentImages } from "../AgentImages";
import { BecomeACreator } from "../BecomeACreator";
import { AgentsSection } from "../composite/AgentsSection";
import { Separator } from "../../ui/separator";
import { IconType } from "../../ui/icons";
import { BreadCrumbs } from "../BreadCrumbs";
import { Navbar } from "@/components/agptui/Navbar";
import { AgentInfo } from "@/components/agptui/AgentInfo";
import { AgentImages } from "@/components/agptui/AgentImages";
import { BecomeACreator } from "@/components/agptui/BecomeACreator";
import { AgentsSection } from "@/components/agptui/composite/AgentsSection";
import { Separator } from "@/components/ui/separator";
import { IconType } from "@/components/ui/icons";
import { BreadCrumbs } from "@/components/agptui/BreadCrumbs";
interface AgentPageProps {
isLoggedIn: boolean;
userName: string;
userEmail: string;
navLinks: { name: string; href: string }[];
@@ -52,6 +53,7 @@ interface AgentPageProps {
}
export const AgentPage: React.FC<AgentPageProps> = ({
isLoggedIn,
userName,
userEmail,
navLinks,
@@ -85,6 +87,7 @@ export const AgentPage: React.FC<AgentPageProps> = ({
return (
<div className="mx-auto w-screen max-w-[1360px]">
<Navbar
isLoggedIn={isLoggedIn}
userName={userName}
userEmail={userEmail}
links={navLinks}

View File

@@ -13,6 +13,9 @@ const meta: Meta<typeof CreatorDashboardPage> = {
},
},
tags: ["autodocs"],
argTypes: {
isLoggedIn: { control: "boolean" },
},
};
export default meta;
@@ -97,6 +100,7 @@ const sampleAgents = [
export const Default: Story = {
args: {
isLoggedIn: true,
userName: "John Doe",
userEmail: "john.doe@example.com",
navLinks: sampleNavLinks,

View File

@@ -1,12 +1,13 @@
import * as React from "react";
import { Navbar } from "../Navbar";
import { Sidebar } from "../Sidebar";
import { AgentTable } from "../AgentTable";
import { Button } from "../Button";
import { Navbar } from "@/components/agptui/Navbar";
import { Sidebar } from "@/components/agptui/Sidebar";
import { AgentTable } from "@/components/agptui/AgentTable";
import { Button } from "@/components/agptui/Button";
import { Separator } from "@/components/ui/separator";
import { IconType } from "../../ui/icons";
import { IconType } from "@/components/ui/icons";
interface CreatorDashboardPageProps {
isLoggedIn: boolean;
userName: string;
userEmail: string;
navLinks: { name: string; href: string }[];
@@ -39,6 +40,7 @@ interface CreatorDashboardPageProps {
}
export const CreatorDashboardPage: React.FC<CreatorDashboardPageProps> = ({
isLoggedIn,
userName,
userEmail,
navLinks,
@@ -50,6 +52,7 @@ export const CreatorDashboardPage: React.FC<CreatorDashboardPageProps> = ({
return (
<div className="mx-auto w-full max-w-[1440px] bg-white">
<Navbar
isLoggedIn={isLoggedIn}
userName={userName}
userEmail={userEmail}
links={navLinks}

View File

@@ -15,6 +15,7 @@ const meta = {
},
tags: ["autodocs"],
argTypes: {
isLoggedIn: { control: "boolean" },
userName: { control: "text" },
userEmail: { control: "text" },
navLinks: { control: "object" },
@@ -76,6 +77,7 @@ const mockCreatorInfo = {
avgRating: 4.8,
agentCount: 15,
topCategories: ["SEO", "Marketing", "Data Analysis"],
avatarSrc: "https://example.com/avatar1.jpg",
otherLinks: {
website: "https://ailabs.com",
github: "https://github.com/ailabs",
@@ -158,6 +160,7 @@ const mockCreatorAgents = [
export const Default: Story = {
args: {
isLoggedIn: true,
userName: "John Doe",
userEmail: "john.doe@example.com",
navLinks: mockNavLinks,

View File

@@ -1,11 +1,12 @@
import * as React from "react";
import { Navbar } from "../Navbar";
import { CreatorDetails } from "../composite/CreatorDetails";
import { AgentsSection } from "../composite/AgentsSection";
import { BreadCrumbs } from "../BreadCrumbs";
import { IconType } from "../../ui/icons";
import { Navbar } from "@/components/agptui/Navbar";
import { CreatorDetails } from "@/components/agptui/composite/CreatorDetails";
import { AgentsSection } from "@/components/agptui/composite/AgentsSection";
import { BreadCrumbs } from "@/components/agptui/BreadCrumbs";
import { IconType } from "@/components/ui/icons";
interface CreatorPageProps {
isLoggedIn: boolean;
userName: string;
userEmail: string;
navLinks: { name: string; href: string }[];
@@ -44,6 +45,7 @@ interface CreatorPageProps {
}
export const CreatorPage: React.FC<CreatorPageProps> = ({
isLoggedIn,
userName,
userEmail,
navLinks,
@@ -65,6 +67,7 @@ export const CreatorPage: React.FC<CreatorPageProps> = ({
return (
<div className="mx-auto w-screen max-w-[1440px]">
<Navbar
isLoggedIn={isLoggedIn}
userName={userName}
userEmail={userEmail}
links={navLinks}

View File

@@ -15,6 +15,7 @@ const meta = {
},
tags: ["autodocs"],
argTypes: {
isLoggedIn: { control: "boolean" },
userName: { control: "text" },
navLinks: { control: "object" },
activeLink: { control: "text" },
@@ -237,6 +238,7 @@ const mockFeaturedCreators = [
export const Default: Story = {
args: {
isLoggedIn: true,
userName: "John Doe",
userEmail: "john.doe@example.com",
navLinks: mockNavLinks,
@@ -272,6 +274,7 @@ export const WithInteraction: Story = {
export const EmptyState: Story = {
args: {
isLoggedIn: false,
userName: "Jane Smith",
userEmail: "jane.smith@example.com",
navLinks: mockNavLinks,

View File

@@ -1,13 +1,14 @@
import * as React from "react";
import { Navbar } from "../Navbar";
import { HeroSection } from "../composite/HeroSection";
import { FeaturedSection } from "../composite/FeaturedSection";
import { AgentsSection } from "../composite/AgentsSection";
import { BecomeACreator } from "../BecomeACreator";
import { FeaturedCreators } from "../composite/FeaturedCreators";
import { Separator } from "../../ui/separator";
import { IconType } from "../../ui/icons";
import { Navbar } from "@/components/agptui/Navbar";
import { HeroSection } from "@/components/agptui/composite/HeroSection";
import { FeaturedSection } from "@/components/agptui/composite/FeaturedSection";
import { AgentsSection } from "@/components/agptui/composite/AgentsSection";
import { BecomeACreator } from "@/components/agptui/BecomeACreator";
import { FeaturedCreators } from "@/components/agptui/composite/FeaturedCreators";
import { Separator } from "@/components/ui/separator";
import { IconType } from "@/components/ui/icons";
interface PageProps {
isLoggedIn: boolean;
userName: string;
userEmail: string;
navLinks: { name: string; href: string }[];
@@ -47,6 +48,7 @@ interface PageProps {
}
export const Page: React.FC<PageProps> = ({
isLoggedIn,
userName,
userEmail,
navLinks,
@@ -84,6 +86,7 @@ export const Page: React.FC<PageProps> = ({
return (
<div className="mx-auto w-screen max-w-[1360px]">
<Navbar
isLoggedIn={isLoggedIn}
userName={userName}
userEmail={userEmail}
links={navLinks}

View File

@@ -12,10 +12,11 @@ const getServerUser = async () => {
data: { user },
error,
} = await supabase.auth.getUser();
if (error) {
console.error("Supabase auth error:", error);
return { user: null, role: null, error: `Auth error: ${error.message}` };
}
// if (error) {
// // FIX: Suppressing error for now. Need to stop the nav bar calling this all the time
// // console.error("Supabase auth error:", error);
// return { user: null, role: null, error: `Auth error: ${error.message}` };
// }
if (!user) {
return { user: null, role: null, error: "No user found in the response" };
}

View File

@@ -271,13 +271,14 @@ export default class BaseAutoGPTServerAPI {
if (!response.ok) {
console.warn(`${method} ${path} returned non-OK response:`, response);
if (
response.status === 403 &&
response.statusText === "Not authenticated" &&
typeof window !== "undefined" // Check if in browser environment
) {
window.location.href = "/login";
}
// console.warn("baseClient is attempting to redirect by changing window location")
// if (
// response.status === 403 &&
// response.statusText === "Not authenticated" &&
// typeof window !== "undefined" // Check if in browser environment
// ) {
// window.location.href = "/login";
// }
let errorDetail;
try {

View File

@@ -1,18 +1,19 @@
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
import { LOCALES } from "@/lib/utils";
// TODO: Update the protected pages list
const PROTECTED_PAGES = ['/monitor', '/build', ]
const ADMIN_PAGES = ['/admin']
const PROTECTED_PAGES = ["/monitor", "/build"];
const ADMIN_PAGES = ["/admin"];
export async function updateSession(request: NextRequest) {
export async function updateSession(request: NextRequest, locale: string) {
let supabaseResponse = NextResponse.next({
request,
});
const isAvailable = Boolean(
process.env.NEXT_PUBLIC_SUPABASE_URL &&
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
);
if (!isAvailable) {
@@ -54,28 +55,47 @@ export async function updateSession(request: NextRequest) {
// Get the user role
const userRole = user?.role;
const url = request.nextUrl.clone();
const pathname = request.nextUrl.pathname;
const pathnameHasLocale = LOCALES.some(
(locale) =>
pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`,
);
const lang = pathnameHasLocale ? `/${pathname.split("/")[1]}` : "";
// AUTH REDIRECTS
// If not logged in and trying to access a protected page, redirect to login
if (
!user &&
(PROTECTED_PAGES.some(page => request.nextUrl.pathname.startsWith(page)) ||
ADMIN_PAGES.some(page => request.nextUrl.pathname.startsWith(page)))
(!user &&
PROTECTED_PAGES.some((page) => {
const combinedPath = `${lang}${page}`;
// console.log("Checking pathname:", request.nextUrl.pathname, "against:", combinedPath);
return request.nextUrl.pathname.startsWith(combinedPath);
})) ||
ADMIN_PAGES.some((page) => {
const combinedPath = `${lang}${page}`;
// console.log("Checking pathname:", request.nextUrl.pathname, "against:", combinedPath);
return request.nextUrl.pathname.startsWith(combinedPath);
})
) {
// no user, potentially respond by redirecting the user to the login page
const url = request.nextUrl.clone();
url.pathname = "/login";
return NextResponse.redirect(url)
url.pathname = `${locale}/login`;
return NextResponse.redirect(url);
}
if (
user && userRole != "admin" &&
ADMIN_PAGES.some(page => request.nextUrl.pathname.startsWith(page))
user &&
userRole != "admin" &&
ADMIN_PAGES.some((page) =>
request.nextUrl.pathname.startsWith(`${lang}${page}`),
)
) {
// no user, potentially respond by redirecting the user to the login page
const url = request.nextUrl.clone();
url.pathname = "/dashboard";
return NextResponse.redirect(url)
url.pathname = `${locale}/monitoring`;
return NextResponse.redirect(url);
}
if (locale) {
url.pathname = `${locale}${pathname}`;
return NextResponse.redirect(url);
}
// IMPORTANT: You *must* return the supabaseResponse object as it is. If you're

View File

@@ -213,3 +213,6 @@ export function getBehaveAs(): BehaveAs {
? BehaveAs.CLOUD
: BehaveAs.LOCAL;
}
export const LOCALES = process.env.NEXT_PUBLIC_LOCALES?.split(",") || ["en"];
export const DEFAULT_LOCALE = process.env.NEXT_PUBLIC_DEFAULT_LOCALE || "en";

View File

@@ -4,18 +4,13 @@ import React from "react";
import * as Sentry from "@sentry/nextjs";
export async function withRoleAccess(allowedRoles: string[]) {
"use server";
console.log("withRoleAccess called:", allowedRoles);
("use server");
return await Sentry.withServerActionInstrumentation(
"withRoleAccess",
{},
async () => {
return async function <T extends React.ComponentType<any>>(Component: T) {
const { user, role, error } = await getServerUser();
if (error || !user || !role || !allowedRoles.includes(role)) {
redirect("/unauthorized");
}
return Component;
};
},

View File

@@ -1,8 +1,30 @@
import { updateSession } from "@/lib/supabase/middleware";
import { type NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { match } from "@formatjs/intl-localematcher";
import Negotiator from "negotiator";
import { LOCALES, DEFAULT_LOCALE } from "@/lib/utils";
function getLocale(request: NextRequest) {
let headers = Object.fromEntries(request.headers.entries());
let languages = new Negotiator({ headers }).languages();
return match(languages, LOCALES, DEFAULT_LOCALE);
}
export async function middleware(request: NextRequest) {
return await updateSession(request);
// Check if there is any supported locale in the pathname
const { pathname } = request.nextUrl;
const pathnameHasLocale = LOCALES.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`,
);
let locale = "";
if (!pathnameHasLocale) {
// Redirect if there is no locale
locale = `/${getLocale(request)}`;
}
return await updateSession(request, locale);
}
export const config = {
@@ -14,6 +36,6 @@ export const config = {
* - favicon.ico (favicon file)
* Feel free to modify this pattern to include more paths.
*/
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
"/((?!_next/static|_next/image|favicon.ico|auth|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
};

View File

@@ -1118,6 +1118,13 @@
resolved "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.7.tgz"
integrity sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA==
"@formatjs/intl-localematcher@^0.5.5":
version "0.5.5"
resolved "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.5.tgz"
integrity sha512-t5tOGMgZ/i5+ALl2/offNqAQq/lfUnKLEw0mXQI4N4bqpedhrSE+fyKLpwnd22sK0dif6AV+ufQcTsKShB9J1g==
dependencies:
tslib "^2.7.0"
"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0":
version "9.3.0"
resolved "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz"
@@ -3596,6 +3603,11 @@
dependencies:
"@types/node" "*"
"@types/negotiator@^0.6.3":
version "0.6.3"
resolved "https://registry.npmjs.org/@types/negotiator/-/negotiator-0.6.3.tgz"
integrity sha512-JkXTOdKs5MF086b/pt8C3+yVp3iDUwG635L7oCH6HvJvvr6lSUU5oe/gLXnPEfYRROHjJIPgCV6cuAg8gGkntQ==
"@types/node@*", "@types/node@^22.0.0", "@types/node@^22.7.3":
version "22.7.4"
resolved "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz"
@@ -9157,6 +9169,11 @@ natural-compare@^1.4.0:
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
negotiator@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz"
integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==
negotiator@0.6.3:
version "0.6.3"
resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz"
@@ -11569,10 +11586,10 @@ tslib@^1.8.1:
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0:
version "2.6.3"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz"
integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.7.0:
version "2.8.0"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz"
integrity sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==
tsutils@^3.21.0:
version "3.21.0"