diff --git a/app/dashboard/admin/page.tsx b/app/dashboard/admin/page.tsx
new file mode 100644
index 0000000..e91d47b
--- /dev/null
+++ b/app/dashboard/admin/page.tsx
@@ -0,0 +1,13 @@
+// This ensures this component is rendered only on the client side
+"use client";
+
+import { Approve } from "@/app/features/Pages/Approve/Approve";
+
+export default function BackOfficeUsersPage() {
+ return (
+
+ {/* This page will now be rendered on the client-side */}
+ hello
+
+ );
+}
diff --git a/app/dashboard/admin/users/page.tsx b/app/dashboard/admin/users/page.tsx
new file mode 100644
index 0000000..9a8be75
--- /dev/null
+++ b/app/dashboard/admin/users/page.tsx
@@ -0,0 +1,12 @@
+// This ensures this component is rendered only on the client side
+"use client";
+
+import Users from "@/app/features/Pages/Admin/Users/users";
+
+export default function BackOfficeUsersPage() {
+ return (
+
+
+
+ );
+}
diff --git a/app/dashboard/layout.tsx b/app/dashboard/layout.tsx
index 31835d6..51ee09c 100644
--- a/app/dashboard/layout.tsx
+++ b/app/dashboard/layout.tsx
@@ -1,6 +1,6 @@
"use client";
-import React from "react";
+import React, { useEffect } from "react";
import { LayoutWrapper } from "../features/dashboard/layout/layoutWrapper";
import { MainContent } from "../features/dashboard/layout/mainContent";
import SideBar from "../features/dashboard/sidebar/Sidebar";
@@ -9,6 +9,14 @@ import Header from "../features/dashboard/header/Header";
const DashboardLayout: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
+ useEffect(() => {
+ // if (process.env.NODE_ENV === "development") {
+ import("../../mock/browser").then(({ worker }) => {
+ worker.start();
+ });
+ // }
+ }, []);
+
return (
diff --git a/app/dashboard/transactions/page.tsx b/app/dashboard/transactions/page.tsx
index cd6af40..f2059f1 100644
--- a/app/dashboard/transactions/page.tsx
+++ b/app/dashboard/transactions/page.tsx
@@ -1,7 +1,7 @@
// This ensures this component is rendered only on the client side
"use client";
-import TransactionTable from "@/app/features/Pages/transactions/Transactions";
+import TransactionTable from "@/app/features/Pages/Transactions/Transactions";
export default function TransactionPage() {
return (
diff --git a/app/features/Pages/Admin/Users/interfaces.ts b/app/features/Pages/Admin/Users/interfaces.ts
new file mode 100644
index 0000000..c9ac346
--- /dev/null
+++ b/app/features/Pages/Admin/Users/interfaces.ts
@@ -0,0 +1,26 @@
+export interface IUser {
+ merchantId: number;
+ name?: string;
+ id: string;
+ username: string;
+ firstName: string;
+ lastName: string;
+ email: string;
+ phone: string;
+ jobTitle: string;
+ enabled: boolean;
+ authorities: string[];
+ allowedMerchantIds: number[];
+ created: string;
+ disabledBy: string | null;
+ disabledDate: string | null;
+ disabledReason: string | null;
+ incidentNotes: boolean;
+ lastLogin: string;
+ lastMandatoryUpdated: string;
+ marketingNewsletter: boolean;
+ releaseNotes: boolean;
+ requiredActions: string[];
+ twoFactorCondition: string;
+ twoFactorCredentials: any[]; // Assuming this is an array that could contain any type of data
+}
diff --git a/app/features/Pages/Admin/Users/users.tsx b/app/features/Pages/Admin/Users/users.tsx
new file mode 100644
index 0000000..7e00e63
--- /dev/null
+++ b/app/features/Pages/Admin/Users/users.tsx
@@ -0,0 +1,131 @@
+"use client";
+import React, { useEffect, useState } from "react";
+import { Card, CardContent, Typography, Chip, Stack } from "@mui/material";
+import { IUser } from "./interfaces";
+import UserRoleCard from "@/app/features/UserRoles/userRoleCard";
+
+const Users = () => {
+ const [data, setData] = useState([
+ {
+ merchantId: 100987998,
+ id: "bc6a8a55-13bc-4538-8255-cd0cec3bb4e9",
+ mame: "Jacob",
+ username: "lspaddy",
+ firstName: "Paddy",
+ lastName: "Man",
+ email: "patrick@omegasys.eu",
+ phone: "",
+ jobTitle: "",
+ enabled: true,
+ authorities: [
+ "ROLE_IIN",
+ "ROLE_FIRST_APPROVER",
+ "ROLE_RULES_ADMIN",
+ "ROLE_TRANSACTION_VIEWER",
+ "ROLE_IIN_ADMIN",
+ "ROLE_USER_PSP_ACCOUNT",
+ ],
+ allowedMerchantIds: [100987998],
+ created: "2025-05-04T15:32:48.432Z",
+ disabledBy: null,
+ disabledDate: null,
+ disabledReason: null,
+ incidentNotes: false,
+ lastLogin: "",
+ lastMandatoryUpdated: "2025-05-04T15:32:48.332Z",
+ marketingNewsletter: false,
+ releaseNotes: false,
+ requiredActions: ["CONFIGURE_TOTP", "UPDATE_PASSWORD"],
+ twoFactorCondition: "required",
+ twoFactorCredentials: [],
+ },
+ {
+ merchantId: 100987998,
+ mame: "Jacob",
+ id: "382eed15-1e21-41fa-b1f3-0c1adb3af714",
+ username: "lsterence",
+ firstName: "Terence",
+ lastName: "User",
+ email: "terence@omegasys.eu",
+ phone: "",
+ jobTitle: "",
+ enabled: true,
+ authorities: ["ROLE_IIN", "ROLE_FIRST_APPROVER", "ROLE_RULES_ADMIN"],
+ allowedMerchantIds: [100987998],
+ created: "2025-05-04T15:32:48.432Z",
+ disabledBy: null,
+ disabledDate: null,
+ disabledReason: null,
+ incidentNotes: false,
+ lastLogin: "",
+ lastMandatoryUpdated: "2025-05-04T15:32:48.332Z",
+ marketingNewsletter: false,
+ releaseNotes: false,
+ requiredActions: ["CONFIGURE_TOTP", "UPDATE_PASSWORD"],
+ twoFactorCondition: "required",
+ twoFactorCredentials: [],
+ },
+ // Add more users if needed
+ ]);
+
+ useEffect(() => {
+ // Only run MSW in the browser environment
+ if (typeof window !== "undefined") {
+ const fetchData = async () => {
+ const response = await fetch(
+ "https://test-bo.paymentiq.io/paymentiq/backoffice/api/v2/users/?includeSubMids=false&size=20&page=0&merchantId=100987998"
+ ); // This would be intercepted by MSW in the browser
+ const data = await response.json();
+ console.log("[DATA]", data);
+ // setData(data);
+ };
+
+ fetchData();
+ }
+ }, []);
+
+ return (
+
+ {data.map((user: IUser) => (
+
+
+ {user.username}
+
+ Merchant ID: {user.merchantId}
+
+
+ {/* You can render more UI here for additional properties */}
+
+
+ {/* Add more chips or UI elements for other data */}
+
+
+
+ ))}
+
+ );
+};
+
+// Fetch data server-side using getServerSideProps
+// export async function getServerSideProps() {
+// // Replace this with your actual API call
+// const res = await fetch("https://api.example.com/users");
+// const data = await res.json();
+
+// // Return the fetched data as props
+// return {
+// props: {
+// result: data, // Assuming data is an array of users
+// },
+// };
+// }
+
+export default Users;
diff --git a/app/features/Pages/DashboardHomePage/DashboardHomePage.tsx b/app/features/Pages/DashboardHomePage/DashboardHomePage.tsx
index cd2aede..4eaf208 100644
--- a/app/features/Pages/DashboardHomePage/DashboardHomePage.tsx
+++ b/app/features/Pages/DashboardHomePage/DashboardHomePage.tsx
@@ -1,13 +1,58 @@
import { Box } from "@mui/material";
import { GeneralHealthCard } from "../../GeneralHealthCard/GeneralHealthCard";
-import { TransactionsOverView } from "../../TransactionsOverView/TransactionsOverView";
import { TransactionsWaitingApproval } from "../../TransactionsWaitingApproval/TransactionsWaitingApproval";
import { FetchReport } from "../../FetchReports/FetchReports";
import { Documentation } from "../../Documentation/Documentation";
import { AccountIQ } from "../../AccountIQ/AccountIQ";
import { WhatsNew } from "../../WhatsNew/WhatsNew";
+import { TransactionsOverView } from "../../TransactionsOverview/TransactionsOverview";
+import { useEffect } from "react";
export const DashboardHomePage = () => {
+ // useEffect to fetch data when the component mounts
+ useEffect(() => {
+ // Construct the URL with query parameters
+ const url =
+ "https://test-bo.paymentiq.io/paymentiq/backoffice/api/v2/metrics/txsummary?merchantId=100987998&fromDate=2025-06-29+19%3A10&toDate=2025-07-01+19%3A09";
+
+ // Perform the fetch request inside useEffect
+ const fetchData = async () => {
+ try {
+ // Start loading
+
+ // Make the API request
+ const response = await fetch(url, {
+ method: "GET", // You can change this to 'POST' if necessary
+ headers: {
+ "Content-Type": "application/json",
+ // Add any other headers you need here
+ },
+ });
+
+ // Check if the response is OK (status code 200-299)
+ if (!response.ok) {
+ throw new Error(`Failed to fetch: ${response.status}`);
+ }
+
+ // Parse the JSON response
+ const data = await response.json();
+ console.log("[DATA]", data);
+
+ // Update the state with the fetched data
+ // setTransactions(data.result || []);
+ } catch (err) {
+ // Handle any errors that occurred during the fetch
+ // setError("Failed to load data, please try again.");
+ console.error("Fetch error:", err);
+ } finally {
+ // Set loading to false when the request is complete
+ // setLoading(false);
+ }
+ };
+
+ // Call the fetch function
+ fetchData();
+ }, []); // Empty dependency
return (
<>
diff --git a/app/features/UserRoles/userRoleCard.tsx b/app/features/UserRoles/userRoleCard.tsx
new file mode 100644
index 0000000..43828bb
--- /dev/null
+++ b/app/features/UserRoles/userRoleCard.tsx
@@ -0,0 +1,118 @@
+import {
+ Card,
+ CardContent,
+ Avatar,
+ Typography,
+ Chip,
+ IconButton,
+ Tooltip,
+ Stack,
+ Box,
+} from "@mui/material";
+import {
+ Edit,
+ Delete,
+ Visibility,
+ VpnKey,
+ InfoOutlined,
+ AdminPanelSettings,
+ History,
+} from "@mui/icons-material";
+
+interface Props {
+ username: string;
+ name: string;
+ email: string;
+ isAdmin: boolean;
+ lastLogin: string;
+ merchants: string[];
+ roles: string[];
+ extraRolesCount?: number;
+}
+
+export default function UserRoleCard({
+ username,
+ name,
+ email,
+ isAdmin,
+ lastLogin,
+ // merchants,
+ roles,
+ extraRolesCount,
+}: Props) {
+ return (
+
+
+ {/* Header */}
+
+ {username.slice(0, 2).toUpperCase()}
+
+ {username}
+ {name}
+ {email}
+
+ {isAdmin && (
+ } label="Admin" size="small" />
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Merchants + Roles */}
+
+ Merchants
+ {/*
+ {merchants.map((m) => (
+
+ ))}
+ */}
+
+
+
+
+ Roles{" "}
+
+
+
+
+
+
+ {roles.map((role) => (
+
+ ))}
+
+ {extraRolesCount && }
+
+
+
+ {/* Footer */}
+
+
+ {lastLogin}
+
+
+
+
+ );
+}
diff --git a/app/features/dashboard/sidebar/Sidebar.tsx b/app/features/dashboard/sidebar/Sidebar.tsx
index 381003f..72d9baa 100644
--- a/app/features/dashboard/sidebar/Sidebar.tsx
+++ b/app/features/dashboard/sidebar/Sidebar.tsx
@@ -1,27 +1,68 @@
"use client";
import DashboardIcon from "@mui/icons-material/Dashboard";
+import { useState } from "react";
import { PAGE_LINKS } from "@/app/features/dashboard/sidebar/SidebarLink.constants";
import PageLinks from "../../../components/PageLinks/PageLinks";
import "./sideBar.scss";
+import KeyboardArrowRightIcon from "@mui/icons-material/KeyboardArrowRight";
+import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
const SideBar = () => {
+ const [openMenus, setOpenMenus] = useState>({});
+
+ const toggleMenu = (title: string) => {
+ setOpenMenus((prev) => ({ ...prev, [title]: !prev[title] }));
+ };
+
return (
- Betrise cashir{" "}
+ Betrise cashir
- {PAGE_LINKS.map((link) => (
-
- ))}
+
+ {PAGE_LINKS.map((link) =>
+ link.children ? (
+
+
toggleMenu(link.title)}
+ className="sidebar__dropdown-button"
+ >
+ {link.icon && }
+ {link.title}
+
+ {!openMenus[link.title] ? (
+
+ ) : (
+
+ )}
+
+
+ {openMenus[link.title] && (
+
+ {link.children.map((child) => (
+
+ ))}
+
+ )}
+
+ ) : (
+
+ )
+ )}
);
};
diff --git a/app/features/dashboard/sidebar/SidebarLink.constants.ts b/app/features/dashboard/sidebar/SidebarLink.constants.ts
index 62c143d..c732888 100644
--- a/app/features/dashboard/sidebar/SidebarLink.constants.ts
+++ b/app/features/dashboard/sidebar/SidebarLink.constants.ts
@@ -8,6 +8,8 @@ import GavelIcon from "@mui/icons-material/Gavel";
import HubIcon from "@mui/icons-material/Hub";
import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings";
import InsightsIcon from "@mui/icons-material/Insights";
+import ListAltIcon from "@mui/icons-material/ListAlt";
+import SettingsIcon from "@mui/icons-material/Settings";
import { ISidebarLink } from "@/app/features/dashboard/sidebar/SidebarLink.interfaces";
export const PAGE_LINKS: ISidebarLink[] = [
@@ -28,7 +30,28 @@ export const PAGE_LINKS: ISidebarLink[] = [
// { title: 'Analytics', path: '/analytics', icon: BarChartIcon },
{ title: "Rules", path: "/dashboard/rules", icon: GavelIcon },
{ title: "Rules Hub", path: "/dashboard/rules-hub", icon: HubIcon },
- { title: "Admin", path: "/dashboard/admin", icon: AdminPanelSettingsIcon },
+ {
+ title: "Admin",
+ path: "/dashboard/admin",
+ icon: AdminPanelSettingsIcon,
+ children: [
+ {
+ title: "Manage Users",
+ path: "/dashboard/admin/users",
+ icon: PeopleIcon,
+ },
+ {
+ title: "Transactions",
+ path: "/dashboard/admin/transactions",
+ icon: ListAltIcon,
+ },
+ {
+ title: "Settings",
+ path: "dashboard/admin/settings",
+ icon: SettingsIcon,
+ },
+ ],
+ },
{ title: "Account IQ", path: "/dashboard/account-iq", icon: InsightsIcon },
// { title: 'Documentation', path: '/documentation', icon: DescriptionIcon },
// { title: 'Support', path: '/support', icon: SupportAgentIcon },
diff --git a/app/features/dashboard/sidebar/SidebarLink.interfaces.ts b/app/features/dashboard/sidebar/SidebarLink.interfaces.ts
index 8de910e..d274129 100644
--- a/app/features/dashboard/sidebar/SidebarLink.interfaces.ts
+++ b/app/features/dashboard/sidebar/SidebarLink.interfaces.ts
@@ -4,4 +4,5 @@ export interface ISidebarLink {
title: string;
path: string;
icon?: ElementType;
+ children?: ISidebarLink[];
}
diff --git a/app/features/dashboard/sidebar/sideBar.scss b/app/features/dashboard/sidebar/sideBar.scss
index 04d49db..45b1832 100644
--- a/app/features/dashboard/sidebar/sideBar.scss
+++ b/app/features/dashboard/sidebar/sideBar.scss
@@ -1,27 +1,74 @@
.sidebar {
- position: fixed;
- top: 0;
- left: 0;
- width: 240px;
- height: 100vh;
- background-color: var(--background-primary);
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 240px;
+ height: 100vh;
+ background-color: var(--background-primary);
+ color: white;
+ display: flex;
+ flex-direction: column;
+ padding: 16px;
+ z-index: 1100;
+ border-right: 1px solid #333;
+
+ &__header {
+ font-size: 20px;
+ font-weight: 600;
+ margin-bottom: 24px;
+ display: flex;
+ align-items: center;
color: white;
+
+ &__icon-spacing {
+ margin-left: 8px;
+ }
+ }
+
+ &__dropdown-button {
+ padding: 10px 0;
+ background: none;
+ border: none;
+ color: white;
+ font-size: 16px;
+ text-align: left;
+ display: flex;
+ align-items: center;
+ width: 100%;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+
+ &:hover {
+ background-color: rgba(255, 255, 255, 0.08);
+ }
+
+ svg {
+ margin-right: 10px;
+ }
+
+ .sidebar__arrow {
+ margin-left: auto;
+ font-size: 12px;
+ }
+ }
+
+ &__submenu {
+ margin-left: 28px;
display: flex;
flex-direction: column;
- padding: 16px;
- z-index: 1100;
- border-right: 1px solid #333;
+ border-left: 2px solid rgba(255, 255, 255, 0.1);
+ padding-left: 8px;
- .sidebar__header {
- font-size: 20px;
- font-weight: 600;
- margin-bottom: 24px;
- display: flex;
- align-items: center;
- color: white;
+ a {
+ color: white;
+ padding: 8px 0;
+ font-size: 14px;
+ text-decoration: none;
+ transition: color 0.2s ease;
- .sidebar__icon-spacing {
- margin-left: 8px;
- }
+ &:hover {
+ color: var(--primary-color);
+ }
}
+ }
}
diff --git a/app/page.tsx b/app/page.tsx
new file mode 100644
index 0000000..bb91a4b
--- /dev/null
+++ b/app/page.tsx
@@ -0,0 +1,9 @@
+"use client";
+
+import { DashboardHomePage } from "./features/Pages/DashboardHomePage/DashboardHomePage";
+
+const DashboardPage = () => {
+ return ;
+};
+
+export default DashboardPage;
diff --git a/mock/browser.ts b/mock/browser.ts
new file mode 100644
index 0000000..975f2cd
--- /dev/null
+++ b/mock/browser.ts
@@ -0,0 +1,5 @@
+// mocks/browser.ts
+import { setupWorker } from "msw/browser";
+import { handlers } from "./handlers";
+
+export const worker = setupWorker(...handlers);
diff --git a/mock/handlers.ts b/mock/handlers.ts
new file mode 100644
index 0000000..6db727a
--- /dev/null
+++ b/mock/handlers.ts
@@ -0,0 +1,96 @@
+import { http, HttpResponse } from "msw";
+
+export const handlers = [
+ http.get(
+ "https://test-bo.paymentiq.io/paymentiq/backoffice/api/v2/metrics/txsummary",
+ (req, _res, _ctx) => {
+ const merchantId = req.url.searchParams.get("merchantId");
+ const fromDate = req.url.searchParams.get("fromDate");
+ const toDate = req.url.searchParams.get("toDate");
+
+ console.log(merchantId, fromDate, toDate);
+
+ return HttpResponse.json({
+ result: {
+ txCount: { total: 0, successful: 0 },
+ amount: { value: "0", currency: "EUR" },
+ },
+ });
+ }
+ ),
+ http.get(
+ "https://test-bo.paymentiq.io/paymentiq/backoffice/api/v2/users/",
+ (req, _res, _ctx) => {
+ // Mock data for merchantId = 100987998
+ if (true) {
+ return HttpResponse.json({
+ result: [
+ {
+ merchantId: 100987998,
+ id: "bc6a8a55-13bc-4538-8255-cd0cec3bb4e9",
+ username: "lspaddy",
+ firstName: "Paddy",
+ lastName: "Man",
+ email: "patrick@omegasys.eu",
+ phone: "",
+ jobTitle: "",
+ enabled: true,
+ authorities: [
+ "ROLE_IIN",
+ "ROLE_FIRST_APPROVER",
+ "ROLE_RULES_ADMIN",
+ "ROLE_TRANSACTION_VIEWER",
+ "ROLE_IIN_ADMIN",
+ "ROLE_USER_PSP_ACCOUNT",
+ ],
+ allowedMerchantIds: [100987998],
+ created: "2025-05-04T15:32:48.432Z",
+ disabledBy: null,
+ disabledDate: null,
+ disabledReason: null,
+ incidentNotes: false,
+ lastLogin: "",
+ lastMandatoryUpdated: "2025-05-04T15:32:48.332Z",
+ marketingNewsletter: false,
+ releaseNotes: false,
+ requiredActions: ["CONFIGURE_TOTP", "UPDATE_PASSWORD"],
+ twoFactorCondition: "required",
+ twoFactorCredentials: [],
+ },
+ {
+ merchantId: 100987998,
+ id: "382eed15-1e21-41fa-b1f3-0c1adb3af714",
+ username: "lsterence",
+ firstName: "Terence",
+ lastName: "User",
+ email: "terence@omegasys.eu",
+ phone: "",
+ jobTitle: "",
+ enabled: true,
+ authorities: [
+ "ROLE_IIN",
+ "ROLE_FIRST_APPROVER",
+ "ROLE_RULES_ADMIN",
+ ],
+ allowedMerchantIds: [100987998],
+ created: "2025-05-04T15:32:48.432Z",
+ disabledBy: null,
+ disabledDate: null,
+ disabledReason: null,
+ incidentNotes: false,
+ lastLogin: "",
+ lastMandatoryUpdated: "2025-05-04T15:32:48.332Z",
+ marketingNewsletter: false,
+ releaseNotes: false,
+ requiredActions: ["CONFIGURE_TOTP", "UPDATE_PASSWORD"],
+ twoFactorCondition: "required",
+ twoFactorCredentials: [],
+ },
+ // Add more users if needed
+ ],
+ total: 4,
+ });
+ }
+ }
+ ),
+];
diff --git a/package.json b/package.json
index ea98c4e..d3f094a 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,12 @@
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.3.3",
+ "msw": "^2.10.2",
"typescript": "^5"
+ },
+ "msw": {
+ "workerDirectory": [
+ "public"
+ ]
}
-}
+}
\ No newline at end of file
diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js
new file mode 100644
index 0000000..de7bc0f
--- /dev/null
+++ b/public/mockServiceWorker.js
@@ -0,0 +1,344 @@
+/* eslint-disable */
+/* tslint:disable */
+
+/**
+ * Mock Service Worker.
+ * @see https://github.com/mswjs/msw
+ * - Please do NOT modify this file.
+ */
+
+const PACKAGE_VERSION = '2.10.2'
+const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
+const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
+const activeClientIds = new Set()
+
+addEventListener('install', function () {
+ self.skipWaiting()
+})
+
+addEventListener('activate', function (event) {
+ event.waitUntil(self.clients.claim())
+})
+
+addEventListener('message', async function (event) {
+ const clientId = Reflect.get(event.source || {}, 'id')
+
+ if (!clientId || !self.clients) {
+ return
+ }
+
+ const client = await self.clients.get(clientId)
+
+ if (!client) {
+ return
+ }
+
+ const allClients = await self.clients.matchAll({
+ type: 'window',
+ })
+
+ switch (event.data) {
+ case 'KEEPALIVE_REQUEST': {
+ sendToClient(client, {
+ type: 'KEEPALIVE_RESPONSE',
+ })
+ break
+ }
+
+ case 'INTEGRITY_CHECK_REQUEST': {
+ sendToClient(client, {
+ type: 'INTEGRITY_CHECK_RESPONSE',
+ payload: {
+ packageVersion: PACKAGE_VERSION,
+ checksum: INTEGRITY_CHECKSUM,
+ },
+ })
+ break
+ }
+
+ case 'MOCK_ACTIVATE': {
+ activeClientIds.add(clientId)
+
+ sendToClient(client, {
+ type: 'MOCKING_ENABLED',
+ payload: {
+ client: {
+ id: client.id,
+ frameType: client.frameType,
+ },
+ },
+ })
+ break
+ }
+
+ case 'MOCK_DEACTIVATE': {
+ activeClientIds.delete(clientId)
+ break
+ }
+
+ case 'CLIENT_CLOSED': {
+ activeClientIds.delete(clientId)
+
+ const remainingClients = allClients.filter((client) => {
+ return client.id !== clientId
+ })
+
+ // Unregister itself when there are no more clients
+ if (remainingClients.length === 0) {
+ self.registration.unregister()
+ }
+
+ break
+ }
+ }
+})
+
+addEventListener('fetch', function (event) {
+ // Bypass navigation requests.
+ if (event.request.mode === 'navigate') {
+ return
+ }
+
+ // Opening the DevTools triggers the "only-if-cached" request
+ // that cannot be handled by the worker. Bypass such requests.
+ if (
+ event.request.cache === 'only-if-cached' &&
+ event.request.mode !== 'same-origin'
+ ) {
+ return
+ }
+
+ // Bypass all requests when there are no active clients.
+ // Prevents the self-unregistered worked from handling requests
+ // after it's been deleted (still remains active until the next reload).
+ if (activeClientIds.size === 0) {
+ return
+ }
+
+ const requestId = crypto.randomUUID()
+ event.respondWith(handleRequest(event, requestId))
+})
+
+/**
+ * @param {FetchEvent} event
+ * @param {string} requestId
+ */
+async function handleRequest(event, requestId) {
+ const client = await resolveMainClient(event)
+ const requestCloneForEvents = event.request.clone()
+ const response = await getResponse(event, client, requestId)
+
+ // Send back the response clone for the "response:*" life-cycle events.
+ // Ensure MSW is active and ready to handle the message, otherwise
+ // this message will pend indefinitely.
+ if (client && activeClientIds.has(client.id)) {
+ const serializedRequest = await serializeRequest(requestCloneForEvents)
+
+ // Clone the response so both the client and the library could consume it.
+ const responseClone = response.clone()
+
+ sendToClient(
+ client,
+ {
+ type: 'RESPONSE',
+ payload: {
+ isMockedResponse: IS_MOCKED_RESPONSE in response,
+ request: {
+ id: requestId,
+ ...serializedRequest,
+ },
+ response: {
+ type: responseClone.type,
+ status: responseClone.status,
+ statusText: responseClone.statusText,
+ headers: Object.fromEntries(responseClone.headers.entries()),
+ body: responseClone.body,
+ },
+ },
+ },
+ responseClone.body ? [serializedRequest.body, responseClone.body] : [],
+ )
+ }
+
+ return response
+}
+
+/**
+ * Resolve the main client for the given event.
+ * Client that issues a request doesn't necessarily equal the client
+ * that registered the worker. It's with the latter the worker should
+ * communicate with during the response resolving phase.
+ * @param {FetchEvent} event
+ * @returns {Promise}
+ */
+async function resolveMainClient(event) {
+ const client = await self.clients.get(event.clientId)
+
+ if (activeClientIds.has(event.clientId)) {
+ return client
+ }
+
+ if (client?.frameType === 'top-level') {
+ return client
+ }
+
+ const allClients = await self.clients.matchAll({
+ type: 'window',
+ })
+
+ return allClients
+ .filter((client) => {
+ // Get only those clients that are currently visible.
+ return client.visibilityState === 'visible'
+ })
+ .find((client) => {
+ // Find the client ID that's recorded in the
+ // set of clients that have registered the worker.
+ return activeClientIds.has(client.id)
+ })
+}
+
+/**
+ * @param {FetchEvent} event
+ * @param {Client | undefined} client
+ * @param {string} requestId
+ * @returns {Promise}
+ */
+async function getResponse(event, client, requestId) {
+ // Clone the request because it might've been already used
+ // (i.e. its body has been read and sent to the client).
+ const requestClone = event.request.clone()
+
+ function passthrough() {
+ // Cast the request headers to a new Headers instance
+ // so the headers can be manipulated with.
+ const headers = new Headers(requestClone.headers)
+
+ // Remove the "accept" header value that marked this request as passthrough.
+ // This prevents request alteration and also keeps it compliant with the
+ // user-defined CORS policies.
+ const acceptHeader = headers.get('accept')
+ if (acceptHeader) {
+ const values = acceptHeader.split(',').map((value) => value.trim())
+ const filteredValues = values.filter(
+ (value) => value !== 'msw/passthrough',
+ )
+
+ if (filteredValues.length > 0) {
+ headers.set('accept', filteredValues.join(', '))
+ } else {
+ headers.delete('accept')
+ }
+ }
+
+ return fetch(requestClone, { headers })
+ }
+
+ // Bypass mocking when the client is not active.
+ if (!client) {
+ return passthrough()
+ }
+
+ // Bypass initial page load requests (i.e. static assets).
+ // The absence of the immediate/parent client in the map of the active clients
+ // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
+ // and is not ready to handle requests.
+ if (!activeClientIds.has(client.id)) {
+ return passthrough()
+ }
+
+ // Notify the client that a request has been intercepted.
+ const serializedRequest = await serializeRequest(event.request)
+ const clientMessage = await sendToClient(
+ client,
+ {
+ type: 'REQUEST',
+ payload: {
+ id: requestId,
+ ...serializedRequest,
+ },
+ },
+ [serializedRequest.body],
+ )
+
+ switch (clientMessage.type) {
+ case 'MOCK_RESPONSE': {
+ return respondWithMock(clientMessage.data)
+ }
+
+ case 'PASSTHROUGH': {
+ return passthrough()
+ }
+ }
+
+ return passthrough()
+}
+
+/**
+ * @param {Client} client
+ * @param {any} message
+ * @param {Array} transferrables
+ * @returns {Promise}
+ */
+function sendToClient(client, message, transferrables = []) {
+ return new Promise((resolve, reject) => {
+ const channel = new MessageChannel()
+
+ channel.port1.onmessage = (event) => {
+ if (event.data && event.data.error) {
+ return reject(event.data.error)
+ }
+
+ resolve(event.data)
+ }
+
+ client.postMessage(message, [
+ channel.port2,
+ ...transferrables.filter(Boolean),
+ ])
+ })
+}
+
+/**
+ * @param {Response} response
+ * @returns {Response}
+ */
+function respondWithMock(response) {
+ // Setting response status code to 0 is a no-op.
+ // However, when responding with a "Response.error()", the produced Response
+ // instance will have status code set to 0. Since it's not possible to create
+ // a Response instance with status code 0, handle that use-case separately.
+ if (response.status === 0) {
+ return Response.error()
+ }
+
+ const mockedResponse = new Response(response.body, response)
+
+ Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
+ value: true,
+ enumerable: true,
+ })
+
+ return mockedResponse
+}
+
+/**
+ * @param {Request} request
+ */
+async function serializeRequest(request) {
+ return {
+ url: request.url,
+ mode: request.mode,
+ method: request.method,
+ headers: Object.fromEntries(request.headers.entries()),
+ cache: request.cache,
+ credentials: request.credentials,
+ destination: request.destination,
+ integrity: request.integrity,
+ redirect: request.redirect,
+ referrer: request.referrer,
+ referrerPolicy: request.referrerPolicy,
+ body: await request.arrayBuffer(),
+ keepalive: request.keepalive,
+ }
+}
diff --git a/yarn.lock b/yarn.lock
index ff2afd4..cc739b9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -82,6 +82,28 @@
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.27.1"
+"@bundled-es-modules/cookie@^2.0.1":
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz#b41376af6a06b3e32a15241d927b840a9b4de507"
+ integrity sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==
+ dependencies:
+ cookie "^0.7.2"
+
+"@bundled-es-modules/statuses@^1.0.1":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz#761d10f44e51a94902c4da48675b71a76cc98872"
+ integrity sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==
+ dependencies:
+ statuses "^2.0.1"
+
+"@bundled-es-modules/tough-cookie@^0.1.6":
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz#fa9cd3cedfeecd6783e8b0d378b4a99e52bde5d3"
+ integrity sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==
+ dependencies:
+ "@types/tough-cookie" "^4.0.5"
+ tough-cookie "^4.1.4"
+
"@emnapi/core@^1.4.3":
version "1.4.3"
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.4.3.tgz#9ac52d2d5aea958f67e52c40a065f51de59b77d6"
@@ -435,6 +457,38 @@
resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.2.tgz#ecf19250f8fe35de684aa2b0ec6f773b3447247b"
integrity sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==
+"@inquirer/confirm@^5.0.0":
+ version "5.1.13"
+ resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-5.1.13.tgz#4931515edc63e25d833c9a40ccf1855e8e822dbc"
+ integrity sha512-EkCtvp67ICIVVzjsquUiVSd+V5HRGOGQfsqA4E4vMWhYnB7InUL0pa0TIWt1i+OfP16Gkds8CdIu6yGZwOM1Yw==
+ dependencies:
+ "@inquirer/core" "^10.1.14"
+ "@inquirer/type" "^3.0.7"
+
+"@inquirer/core@^10.1.14":
+ version "10.1.14"
+ resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-10.1.14.tgz#7678b2daaecf32fa2f6e02a03dc235f9620e197f"
+ integrity sha512-Ma+ZpOJPewtIYl6HZHZckeX1STvDnHTCB2GVINNUlSEn2Am6LddWwfPkIGY0IUFVjUUrr/93XlBwTK6mfLjf0A==
+ dependencies:
+ "@inquirer/figures" "^1.0.12"
+ "@inquirer/type" "^3.0.7"
+ ansi-escapes "^4.3.2"
+ cli-width "^4.1.0"
+ mute-stream "^2.0.0"
+ signal-exit "^4.1.0"
+ wrap-ansi "^6.2.0"
+ yoctocolors-cjs "^2.1.2"
+
+"@inquirer/figures@^1.0.12":
+ version "1.0.12"
+ resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-1.0.12.tgz#667d6254cc7ba3b0c010a323d78024a1d30c6053"
+ integrity sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==
+
+"@inquirer/type@^3.0.7":
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-3.0.7.tgz#b46bcf377b3172dbc768fdbd053e6492ad801a09"
+ integrity sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==
+
"@jridgewell/gen-mapping@^0.3.5":
version "0.3.8"
resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz"
@@ -467,6 +521,18 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
+"@mswjs/interceptors@^0.39.1":
+ version "0.39.2"
+ resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.39.2.tgz#de9de0ab23f99d387c7904df7219a92157d1d666"
+ integrity sha512-RuzCup9Ct91Y7V79xwCb146RaBRHZ7NBbrIUySumd1rpKqHL5OonaqrGIbug5hNwP/fRyxFMA6ISgw4FTtYFYg==
+ dependencies:
+ "@open-draft/deferred-promise" "^2.2.0"
+ "@open-draft/logger" "^0.3.0"
+ "@open-draft/until" "^2.0.0"
+ is-node-process "^1.2.0"
+ outvariant "^1.4.3"
+ strict-event-emitter "^0.5.1"
+
"@mui/core-downloads-tracker@^7.1.2":
version "7.1.2"
resolved "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.1.2.tgz"
@@ -672,6 +738,24 @@
resolved "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz"
integrity sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==
+"@open-draft/deferred-promise@^2.2.0":
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz#4a822d10f6f0e316be4d67b4d4f8c9a124b073bd"
+ integrity sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==
+
+"@open-draft/logger@^0.3.0":
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/@open-draft/logger/-/logger-0.3.0.tgz#2b3ab1242b360aa0adb28b85f5d7da1c133a0954"
+ integrity sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==
+ dependencies:
+ is-node-process "^1.2.0"
+ outvariant "^1.4.0"
+
+"@open-draft/until@^2.0.0", "@open-draft/until@^2.1.0":
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-2.1.0.tgz#0acf32f470af2ceaf47f095cdecd40d68666efda"
+ integrity sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==
+
"@parcel/watcher-android-arm64@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz#507f836d7e2042f798c7d07ad19c3546f9848ac1"
@@ -795,6 +879,11 @@
dependencies:
tslib "^2.4.0"
+"@types/cookie@^0.6.0":
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5"
+ integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==
+
"@types/d3-array@^3.0.3":
version "3.2.1"
resolved "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz"
@@ -908,6 +997,16 @@
dependencies:
csstype "^3.0.2"
+"@types/statuses@^2.0.4":
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/@types/statuses/-/statuses-2.0.6.tgz#66748315cc9a96d63403baa8671b2c124f8633aa"
+ integrity sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==
+
+"@types/tough-cookie@^4.0.5":
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304"
+ integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==
+
"@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
version "8.34.1"
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz"
@@ -1127,7 +1226,19 @@ ajv@^6.12.4:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
-ansi-styles@^4.1.0:
+ansi-escapes@^4.3.2:
+ version "4.3.2"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
+ integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
+ dependencies:
+ type-fest "^0.21.3"
+
+ansi-regex@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
+ integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+ansi-styles@^4.0.0, ansi-styles@^4.1.0:
version "4.3.0"
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz"
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
@@ -1369,11 +1480,25 @@ classnames@^2.2.6:
resolved "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz"
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
+cli-width@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-4.1.0.tgz#42daac41d3c254ef38ad8ac037672130173691c5"
+ integrity sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==
+
client-only@0.0.1:
version "0.0.1"
resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
+cliui@^8.0.1:
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
+ integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==
+ dependencies:
+ string-width "^4.2.0"
+ strip-ansi "^6.0.1"
+ wrap-ansi "^7.0.0"
+
clsx@^2.0.0, clsx@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz"
@@ -1422,6 +1547,11 @@ convert-source-map@^1.5.0:
resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz"
integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
+cookie@^0.7.2:
+ version "0.7.2"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
+ integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
+
cosmiconfig@^7.0.0:
version "7.1.0"
resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz"
@@ -1648,6 +1778,11 @@ dunder-proto@^1.0.0, dunder-proto@^1.0.1:
es-errors "^1.3.0"
gopd "^1.2.0"
+emoji-regex@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
+ integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+
emoji-regex@^9.2.2:
version "9.2.2"
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz"
@@ -1785,6 +1920,11 @@ es-to-primitive@^1.3.0:
is-date-object "^1.0.5"
is-symbol "^1.0.4"
+escalade@^3.1.1:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5"
+ integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
+
escape-string-regexp@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz"
@@ -2140,6 +2280,11 @@ functions-have-names@^1.2.3:
resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz"
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
+get-caller-file@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+ integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+
get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0:
version "1.3.0"
resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz"
@@ -2222,6 +2367,11 @@ graphemer@^1.4.0:
resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz"
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
+graphql@^16.8.1:
+ version "16.11.0"
+ resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.11.0.tgz#96d17f66370678027fdf59b2d4c20b4efaa8a633"
+ integrity sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==
+
has-bigints@^1.0.2:
version "1.1.0"
resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz"
@@ -2265,6 +2415,11 @@ hasown@^2.0.2:
dependencies:
function-bind "^1.1.2"
+headers-polyfill@^4.0.2:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-4.0.3.tgz#922a0155de30ecc1f785bcf04be77844ca95ad07"
+ integrity sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==
+
hoist-non-react-statics@^3.3.1:
version "3.3.2"
resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz"
@@ -2407,6 +2562,11 @@ is-finalizationregistry@^1.1.0:
dependencies:
call-bound "^1.0.3"
+is-fullwidth-code-point@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
+ integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+
is-generator-function@^1.0.10:
version "1.1.0"
resolved "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz"
@@ -2434,6 +2594,11 @@ is-negative-zero@^2.0.3:
resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz"
integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==
+is-node-process@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/is-node-process/-/is-node-process-1.2.0.tgz#ea02a1b90ddb3934a19aea414e88edef7e11d134"
+ integrity sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==
+
is-number-object@^1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz"
@@ -2687,6 +2852,35 @@ ms@^2.1.1, ms@^2.1.3:
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+msw@^2.10.2:
+ version "2.10.2"
+ resolved "https://registry.yarnpkg.com/msw/-/msw-2.10.2.tgz#e7a56ed0b6865b00a30b4c4a5b59e5388fd48315"
+ integrity sha512-RCKM6IZseZQCWcSWlutdf590M8nVfRHG1ImwzOtwz8IYxgT4zhUO0rfTcTvDGiaFE0Rhcc+h43lcF3Jc9gFtwQ==
+ dependencies:
+ "@bundled-es-modules/cookie" "^2.0.1"
+ "@bundled-es-modules/statuses" "^1.0.1"
+ "@bundled-es-modules/tough-cookie" "^0.1.6"
+ "@inquirer/confirm" "^5.0.0"
+ "@mswjs/interceptors" "^0.39.1"
+ "@open-draft/deferred-promise" "^2.2.0"
+ "@open-draft/until" "^2.1.0"
+ "@types/cookie" "^0.6.0"
+ "@types/statuses" "^2.0.4"
+ graphql "^16.8.1"
+ headers-polyfill "^4.0.2"
+ is-node-process "^1.2.0"
+ outvariant "^1.4.3"
+ path-to-regexp "^6.3.0"
+ picocolors "^1.1.1"
+ strict-event-emitter "^0.5.1"
+ type-fest "^4.26.1"
+ yargs "^17.7.2"
+
+mute-stream@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-2.0.0.tgz#a5446fc0c512b71c83c44d908d5c7b7b4c493b2b"
+ integrity sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==
+
nanoid@^3.3.6:
version "3.3.11"
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz"
@@ -2808,6 +3002,11 @@ optionator@^0.9.3:
type-check "^0.4.0"
word-wrap "^1.2.5"
+outvariant@^1.4.0, outvariant@^1.4.3:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/outvariant/-/outvariant-1.4.3.tgz#221c1bfc093e8fec7075497e7799fdbf43d14873"
+ integrity sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==
+
own-keys@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz"
@@ -2863,6 +3062,11 @@ path-parse@^1.0.7:
resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+path-to-regexp@^6.3.0:
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz#2b6a26a337737a8e1416f9272ed0766b1c0389f4"
+ integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==
+
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz"
@@ -2911,11 +3115,23 @@ prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
object-assign "^4.1.1"
react-is "^16.13.1"
-punycode@^2.1.0:
+psl@^1.1.33:
+ version "1.15.0"
+ resolved "https://registry.yarnpkg.com/psl/-/psl-1.15.0.tgz#bdace31896f1d97cec6a79e8224898ce93d974c6"
+ integrity sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==
+ dependencies:
+ punycode "^2.3.1"
+
+punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1:
version "2.3.1"
resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
+querystringify@^2.1.1:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
+ integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
+
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
@@ -3034,6 +3250,16 @@ regexp.prototype.flags@^1.5.3, regexp.prototype.flags@^1.5.4:
gopd "^1.2.0"
set-function-name "^2.0.2"
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+ integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
+
+requires-port@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
+ integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
+
reselect@^5.1.1:
version "5.1.1"
resolved "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz"
@@ -3252,6 +3478,11 @@ side-channel@^1.1.0:
side-channel-map "^1.0.1"
side-channel-weakmap "^1.0.2"
+signal-exit@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
+ integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
+
simple-swizzle@^0.2.2:
version "0.2.2"
resolved "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz"
@@ -3281,6 +3512,11 @@ stable-hash@^0.0.5:
resolved "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz"
integrity sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==
+statuses@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382"
+ integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==
+
stop-iteration-iterator@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz"
@@ -3294,6 +3530,20 @@ streamsearch@^1.1.0:
resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz"
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
+strict-event-emitter@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz#1602ece81c51574ca39c6815e09f1a3e8550bd93"
+ integrity sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==
+
+string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
string.prototype.includes@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz"
@@ -3362,6 +3612,13 @@ string.prototype.trimstart@^1.0.8:
define-properties "^1.2.1"
es-object-atoms "^1.0.0"
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz"
@@ -3416,6 +3673,16 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
+tough-cookie@^4.1.4:
+ version "4.1.4"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36"
+ integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==
+ dependencies:
+ psl "^1.1.33"
+ punycode "^2.1.1"
+ universalify "^0.2.0"
+ url-parse "^1.5.3"
+
ts-api-utils@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz"
@@ -3443,6 +3710,16 @@ type-check@^0.4.0, type-check@~0.4.0:
dependencies:
prelude-ls "^1.2.1"
+type-fest@^0.21.3:
+ version "0.21.3"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
+ integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
+
+type-fest@^4.26.1:
+ version "4.41.0"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.41.0.tgz#6ae1c8e5731273c2bf1f58ad39cbae2c91a46c58"
+ integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==
+
typed-array-buffer@^1.0.3:
version "1.0.3"
resolved "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz"
@@ -3508,6 +3785,11 @@ undici-types@~6.21.0:
resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz"
integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==
+universalify@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0"
+ integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==
+
unrs-resolver@^1.6.2:
version "1.9.0"
resolved "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.9.0.tgz"
@@ -3542,6 +3824,14 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
+url-parse@^1.5.3:
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1"
+ integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==
+ dependencies:
+ querystringify "^2.1.1"
+ requires-port "^1.0.0"
+
use-sync-external-store@^1.5.0:
version "1.5.0"
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz"
@@ -3642,6 +3932,24 @@ word@~0.3.0:
resolved "https://registry.npmjs.org/word/-/word-0.3.0.tgz"
integrity sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==
+wrap-ansi@^6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
+ integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
+wrap-ansi@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+ integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
xlsx@^0.18.5:
version "0.18.5"
resolved "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz"
@@ -3655,12 +3963,40 @@ xlsx@^0.18.5:
wmf "~1.0.1"
word "~0.3.0"
+y18n@^5.0.5:
+ version "5.0.8"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
+ integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
+
yaml@^1.10.0:
version "1.10.2"
resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
+yargs-parser@^21.1.1:
+ version "21.1.1"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
+ integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
+
+yargs@^17.7.2:
+ version "17.7.2"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
+ integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
+ dependencies:
+ cliui "^8.0.1"
+ escalade "^3.1.1"
+ get-caller-file "^2.0.5"
+ require-directory "^2.1.1"
+ string-width "^4.2.3"
+ y18n "^5.0.5"
+ yargs-parser "^21.1.1"
+
yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
+
+yoctocolors-cjs@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz#f4b905a840a37506813a7acaa28febe97767a242"
+ integrity sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==