Update dependencies and version number, remove link tickets endpoint

This commit is contained in:
Darren Clarke 2025-10-07 11:24:00 +02:00
parent 6f0f97ab7b
commit 11563a794e
36 changed files with 2953 additions and 4655 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@link-stack/bridge-frontend",
"version": "3.1.0",
"version": "3.2.0b3",
"type": "module",
"scripts": {
"dev": "next dev",
@ -13,26 +13,26 @@
"migrate:down:one": "tsx database/migrate.ts down:one"
},
"dependencies": {
"@auth/kysely-adapter": "^1.8.0",
"@auth/kysely-adapter": "^1.10.0",
"@mui/icons-material": "^6",
"@mui/material": "^6",
"@mui/material-nextjs": "^6",
"@mui/x-license": "^7.28.0",
"@mui/x-license": "^7",
"@link-stack/bridge-common": "*",
"@link-stack/bridge-ui": "*",
"next": "15.2.3",
"next": "15.5.4",
"next-auth": "^4.24.11",
"react": "19.0.0",
"react-dom": "19.0.0",
"sharp": "^0.33.5",
"tsx": "^4.19.3",
"react": "19.2.0",
"react-dom": "19.2.0",
"sharp": "^0.34.4",
"tsx": "^4.20.6",
"@link-stack/ui": "*"
},
"devDependencies": {
"@link-stack/eslint-config": "*",
"@link-stack/typescript-config": "*",
"@types/node": "^22",
"@types/pg": "^8.11.11",
"@types/node": "^24",
"@types/pg": "^8.15.5",
"@types/react": "^19",
"@types/react-dom": "^19",
"@link-stack/eslint-config": "*",

View file

@ -1,6 +1,6 @@
{
"name": "@link-stack/bridge-migrations",
"version": "3.1.0",
"version": "3.2.0b3",
"type": "module",
"scripts": {
"migrate:up:all": "tsx migrate.ts up:all",
@ -10,14 +10,14 @@
},
"dependencies": {
"@link-stack/logger": "*",
"dotenv": "^16.4.7",
"kysely": "0.27.6",
"pg": "^8.14.1",
"tsx": "^4.19.3"
"dotenv": "^17.2.3",
"kysely": "0.27.5",
"pg": "^8.16.3",
"tsx": "^4.20.6"
},
"devDependencies": {
"@types/node": "^22",
"@types/pg": "^8.11.11",
"@types/node": "^24",
"@types/pg": "^8.15.5",
"@link-stack/eslint-config": "*",
"@link-stack/typescript-config": "*",
"typescript": "^5"

View file

@ -1,27 +1,27 @@
{
"name": "@link-stack/bridge-whatsapp",
"version": "3.1.0",
"version": "3.2.0b3",
"main": "build/main/index.js",
"author": "Darren Clarke <darren@redaranj.com>",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@adiwajshing/keyed-db": "0.2.4",
"@hapi/hapi": "^21.4.0",
"@hapi/hapi": "^21.4.3",
"@hapipal/schmervice": "^3.0.0",
"@hapipal/toys": "^4.0.0",
"@link-stack/logger": "*",
"@whiskeysockets/baileys": "^6.7.16",
"hapi-pino": "^12.1.0",
"link-preview-js": "^3.0.14"
"@whiskeysockets/baileys": "^6.7.20",
"hapi-pino": "^13.0.0",
"link-preview-js": "^3.1.0"
},
"devDependencies": {
"@link-stack/eslint-config": "*",
"@link-stack/jest-config": "*",
"@link-stack/typescript-config": "*",
"@types/node": "*",
"dotenv-cli": "^8.0.0",
"tsx": "^4.19.3",
"typescript": "^5.8.2"
"dotenv-cli": "^10.0.0",
"tsx": "^4.20.6",
"typescript": "^5.9.3"
},
"scripts": {
"build": "tsc -p tsconfig.json",

View file

@ -42,7 +42,7 @@ export const SendMessageRoute = withDefaults({
attachmentCount: attachments?.length || 0,
},
"Sent a message at %s",
new Date(),
new Date().toISOString(),
);
return _h
@ -69,7 +69,7 @@ export const ReceiveMessageRoute = withDefaults({
const date = new Date();
const twoDaysAgo = new Date(date.getTime());
twoDaysAgo.setDate(date.getDate() - 2);
request.logger.info({ id }, "Received messages at %s", new Date());
request.logger.info({ id }, "Received messages at %s", new Date().toISOString());
return whatsappService.receive(id, twoDaysAgo);
},

View file

@ -1,6 +1,6 @@
{
"name": "@link-stack/bridge-worker",
"version": "3.1.0",
"version": "3.2.0b3",
"type": "module",
"main": "build/main/index.js",
"author": "Darren Clarke <darren@redaranj.com>",
@ -17,14 +17,14 @@
"@link-stack/signal-api": "*",
"fluent-ffmpeg": "^2.1.3",
"graphile-worker": "^0.16.6",
"remeda": "^2.21.2",
"twilio": "^5.5.1"
"remeda": "^2.32.0",
"twilio": "^5.10.2"
},
"devDependencies": {
"@types/fluent-ffmpeg": "^2.1.27",
"dotenv-cli": "^8.0.0",
"dotenv-cli": "^10.0.0",
"@link-stack/eslint-config": "*",
"@link-stack/typescript-config": "*",
"typescript": "^5.8.2"
"typescript": "^5.9.3"
}
}

View file

@ -1,6 +1,7 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View file

@ -1,6 +1,6 @@
{
"name": "@link-stack/leafcutter",
"version": "3.1.0",
"version": "3.2.0b3",
"scripts": {
"dev": "next dev -p 3001",
"login": "aws sso login --sso-session cdr",
@ -16,35 +16,35 @@
"@emotion/cache": "^11.14.0",
"@emotion/react": "^11.14.0",
"@emotion/server": "^11.11.0",
"@emotion/styled": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@link-stack/leafcutter-ui": "*",
"@link-stack/logger": "*",
"@link-stack/opensearch-common": "*",
"@mui/icons-material": "^6",
"@mui/material": "^6",
"@mui/material-nextjs": "^6",
"@mui/x-date-pickers-pro": "^7.28.0",
"@opensearch-project/opensearch": "^3.4.0",
"@mui/x-date-pickers-pro": "^7",
"@opensearch-project/opensearch": "^3.5.1",
"date-fns": "^4.1.0",
"http-proxy-middleware": "^3.0.3",
"material-ui-popup-state": "^5.3.3",
"next": "15.2.3",
"http-proxy-middleware": "^3.0.5",
"material-ui-popup-state": "^5.3.6",
"next": "15.5.4",
"next-auth": "^4.24.11",
"react": "19.0.0",
"react": "19.2.0",
"react-cookie": "^8.0.1",
"react-cookie-consent": "^9.0.0",
"react-dom": "19.0.0",
"react-dom": "19.2.0",
"react-iframe": "^1.8.5",
"react-polyglot": "^0.7.2",
"sharp": "^0.33.5",
"uuid": "^11.1.0"
"sharp": "^0.34.4",
"uuid": "^13.0.0"
},
"devDependencies": {
"@types/node": "^22.13.12",
"@types/react": "19.0.12",
"@types/uuid": "^10.0.0",
"@types/node": "^24.7.0",
"@types/react": "19.2.2",
"@types/uuid": "^11.0.0",
"@link-stack/eslint-config": "*",
"@link-stack/typescript-config": "*",
"typescript": "5.8.2"
"typescript": "5.9.3"
}
}

View file

@ -1,10 +0,0 @@
import { Metadata } from "next";
import { ZammadWrapper } from "../../(main)/_components/ZammadWrapper";
export const metadata: Metadata = {
title: "Knowledge Base",
};
export default function Page() {
return <ZammadWrapper path="/#knowledge_base/1/locale/en-us" />;
}

View file

@ -1,100 +0,0 @@
"use client";
import { FC, useState } from "react";
import {
Grid,
Button,
Dialog,
DialogActions,
DialogContent,
TextField,
} from "@mui/material";
import { createTicketArticleAction } from "app/_actions/tickets";
interface ArticleCreateDialogProps {
ticketID: string;
open: boolean;
closeDialog: () => void;
kind: string;
recipient?: string;
}
export const ArticleCreateDialog: FC<ArticleCreateDialogProps> = ({
ticketID,
open,
closeDialog,
kind,
recipient,
}) => {
const [body, setBody] = useState("");
const backgroundColor = kind === "note" ? "#FFB620" : "#1982FC";
const color = kind === "note" ? "black" : "white";
const article = {
body,
type: kind,
internal: kind === "note",
};
if (kind === "email") {
article["to"] = recipient;
}
const createArticle = async () => {
await createTicketArticleAction(ticketID, article);
closeDialog();
setBody("");
};
return (
<Dialog open={open} maxWidth="sm" fullWidth>
<DialogContent>
<TextField
label={kind === "note" ? "Write internal note" : "Write reply"}
multiline
rows={10}
fullWidth
value={body}
onChange={(e: any) => setBody(e.target.value)}
/>
</DialogContent>
<DialogActions sx={{ px: 3, pt: 0, pb: 3 }}>
<Grid container justifyContent="space-between">
<Grid item>
<Button
sx={{
backgroundColor: "white",
color: "#666",
fontFamily: "Poppins, sans-serif",
fontWeight: 700,
borderRadius: 2,
textTransform: "none",
}}
onClick={() => {
setBody("");
closeDialog();
}}
>
Cancel
</Button>
</Grid>
<Grid item>
<Button
sx={{
backgroundColor,
color,
fontFamily: "Poppins, sans-serif",
fontWeight: 700,
borderRadius: 2,
textTransform: "none",
px: 3,
}}
onClick={createArticle}
>
{kind === "note" ? "Save Note" : "Send Reply"}
</Button>
</Grid>
</Grid>
</DialogActions>
</Dialog>
);
};

View file

@ -1,177 +0,0 @@
"use client";
import { FC, useState, useEffect } from "react";
import { getTicketAction, getTicketArticlesAction } from "app/_actions/tickets";
import { Grid, Box, Typography } from "@mui/material";
import { Button, fonts, colors } from "@link-stack/ui";
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
import {
MainContainer,
ChatContainer,
MessageList,
Message,
ConversationHeader,
} from "@chatscope/chat-ui-kit-react";
import { ArticleCreateDialog } from "./ArticleCreateDialog";
interface TicketDetailProps {
id: string;
}
export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
const [ticket, setTicket] = useState<any>(null);
const [ticketArticles, setTicketArticles] = useState<any>(null);
const { poppins, roboto } = fonts;
const { veryLightGray, lightGray } = colors;
const [dialogOpen, setDialogOpen] = useState(false);
const [articleKind, setArticleKind] = useState("note");
useEffect(() => {
const fetchTicket = async () => {
const result = await getTicketAction(id);
setTicket(result);
};
fetchTicket();
const interval = setInterval(fetchTicket, 20000);
return () => clearInterval(interval);
}, [id]);
useEffect(() => {
const fetchTicketArticles = async () => {
const result = await getTicketArticlesAction(id);
setTicketArticles(result);
};
fetchTicketArticles();
const interval = setInterval(fetchTicketArticles, 5000);
return () => clearInterval(interval);
}, [id]);
const closeDialog = () => setDialogOpen(false);
const firstArticle = ticketArticles?.edges[0]?.node;
const firstArticleKind = firstArticle?.type?.name ?? "phone";
const firstEmailSender = firstArticle?.from?.parsed?.[0]?.emailAddress ?? "";
const recipient = firstEmailSender;
const shouldRender = !!ticket && !!ticketArticles;
return (
<Box sx={{ height: "100%", width: "100%", background: veryLightGray }}>
{shouldRender && (
<>
<MainContainer>
<ChatContainer>
<ConversationHeader>
<ConversationHeader.Content>
<Box
sx={{
width: "100%",
textAlign: "center",
fontWeight: "bold",
}}
>
<Typography
variant="h5"
sx={{
fontFamily: poppins.style.fontFamily,
fontWeight: 700,
}}
>
{ticket.title}
</Typography>
<Typography
variant="h6"
sx={{
fontFamily: roboto.style.fontFamily,
fontWeight: 400,
}}
>{`Ticket #${ticket.number} (created ${new Date(
ticket.createdAt,
).toLocaleDateString()})`}</Typography>
</Box>
</ConversationHeader.Content>
</ConversationHeader>
<MessageList style={{ marginBottom: 80 }}>
{ticketArticles.edges.map(({ node: article }: any) => (
<Message
key={article.id}
className={
article.internal
? "internal-note"
: article?.sender?.name === "Agent"
? "outgoing-message"
: "incoming-message"
}
model={{
message: article.bodyWithUrls,
type:
article.contentType === "text/html" ? "html" : "text",
sentTime: article.updated_at,
sender: article.from,
direction:
article.sender === "Agent" ? "outgoing" : "incoming",
position: "single",
}}
/>
))}
</MessageList>
</ChatContainer>
<Box
sx={{
height: 80,
background: veryLightGray,
borderTop: `1px solid ${lightGray}`,
position: "absolute",
bottom: 0,
width: "100%",
zIndex: 1000,
}}
>
<Grid
container
spacing={6}
justifyContent="center"
alignItems="center"
alignContent="center"
sx={{ height: "100%", pt: 6 }}
>
<Grid item>
<Button
text="Write note to agent"
color="#FFB620"
onClick={() => {
setArticleKind("note");
setDialogOpen(true);
}}
/>
</Grid>
<Grid item>
<Button
text="Reply to ticket"
kind="primary"
onClick={() => {
setArticleKind(firstArticleKind);
setDialogOpen(true);
}}
/>
</Grid>
</Grid>
</Box>
</MainContainer>
<ArticleCreateDialog
ticketID={ticket.internalId}
open={dialogOpen}
closeDialog={closeDialog}
kind={articleKind}
recipient={recipient}
/>
</>
)}
</Box>
);
};

View file

@ -1,13 +0,0 @@
import { TicketDetail } from "./_components/TicketDetail";
type PageProps = {
params: Promise<{
id: string;
}>;
};
export default async function Page({ params }: PageProps) {
const { id } = await params;
return <TicketDetail id={id} />;
}

View file

@ -1,201 +0,0 @@
"use client";
import { FC, useEffect, useState } from "react";
import { Grid, Box } from "@mui/material";
import { Select, Button } from "@link-stack/ui";
import { MuiChipsInput } from "mui-chips-input";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import {
updateTicketAction,
getTicketAction,
getTicketStatesAction,
getTicketPrioritiesAction,
} from "app/_actions/tickets";
import { getAgentsAction } from "app/_actions/users";
import { getGroupsAction } from "app/_actions/groups";
interface TicketEditProps {
id: string;
}
export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
const [ticket, setTicket] = useState<any>();
const [hasChanges, setHasChanges] = useState(false);
const [formState, setFormState] = useState({
values: {
group: null,
owner: null,
priority: null,
pendingTime: null,
state: null,
tags: [],
},
});
const [ticketStates, setTicketStates] = useState<any>();
const [ticketPriorities, setTicketPriorities] = useState<any>();
const [groups, setGroups] = useState<any>();
const [agents, setAgents] = useState<any>();
const [pendingVisible, setPendingVisible] = useState(false);
useEffect(() => {
const fetchAgents = async () => {
const groupID = formState?.values?.group?.split("/")?.pop();
const result = await getAgentsAction(groupID);
setAgents(result);
};
const fetchGroups = async () => {
const result = await getGroupsAction();
setGroups(result);
};
const fetchTicketStates = async () => {
const result = await getTicketStatesAction();
setTicketStates(result);
};
const fetchTicketPriorities = async () => {
const result = await getTicketPrioritiesAction();
setTicketPriorities(result);
};
fetchTicketStates();
fetchTicketPriorities();
fetchAgents();
fetchGroups();
}, [formState.values.group]);
useEffect(() => {
const fetchTicket = async () => {
const result = await getTicketAction(id);
setTicket(result);
setFormState({
values: {
...formState.values,
group: result?.group?.id,
owner: result?.owner?.id,
priority: result?.priority?.id,
state: result?.state?.id,
tags: result?.tags,
},
});
};
fetchTicket();
}, []);
const updateFormState = (name: string, value: any) => {
setFormState({
values: {
...formState.values,
[name]: value,
},
});
const stateName = ticketStates?.find(
(state: any) => state.id === formState.values.state,
)?.name;
setPendingVisible(stateName?.includes("pending") ?? false);
setHasChanges(true);
};
const updateTicket = async () => {
await updateTicketAction(id, formState.values);
setHasChanges(false);
};
const shouldRender = !!ticket;
return (
<Box sx={{ height: "100vh", background: "#ddd", p: 2 }}>
{shouldRender && (
<Grid container direction="column" spacing={3}>
<Grid item>
<Box sx={{ m: 1 }}>Group</Box>
<Select
name="group"
label="Group"
formState={formState}
updateFormState={updateFormState}
getOptions={() => groups}
/>
</Grid>
<Grid item>
<Box sx={{ m: 1, mt: 0 }}>Owner</Box>
<Select
name="owner"
label="Owner"
formState={formState}
updateFormState={updateFormState}
getOptions={() => agents}
/>
</Grid>
<Grid item xs={12}>
<Box sx={{ m: 1, mt: 0 }}>State</Box>
<Select
name="state"
label="State"
formState={formState}
updateFormState={updateFormState}
getOptions={() => ticketStates}
/>
</Grid>
<Grid
item
xs={12}
sx={{ display: pendingVisible ? "inherit" : "none" }}
>
<DatePicker
label="Pending Date"
value={new Date(formState.values.pendingTime)}
onChange={(newValue: any) => {
updateFormState("pendingDate", newValue.toISOString());
}}
slotProps={{ textField: { size: "small" } }}
sx={{
width: "100%",
backgroundColor: "white",
}}
/>
</Grid>
<Grid item>
<Box sx={{ m: 1, mt: 0 }}>Priority</Box>
<Select
name="priority"
label="Priority"
formState={formState}
updateFormState={updateFormState}
getOptions={() => ticketPriorities}
/>
</Grid>
<Grid item>
<Box sx={{ mb: 1 }}>Tags</Box>
<MuiChipsInput
sx={{ backgroundColor: "white", width: "100%" }}
value={formState.values.tags}
hideClearAll
onChange={(tags: any) => {
updateFormState("tags", tags);
}}
onDeleteChip={(tag: any) => {
const tags = formState.values.tags.filter(
(t: any) => t !== tag,
);
updateFormState("tags", tags);
}}
/>
</Grid>
<Grid item container direction="row-reverse">
<Grid item>
<Button
text="Save"
kind="primary"
onClick={updateTicket}
disabled={!hasChanges}
/>
</Grid>
</Grid>
</Grid>
)}
</Box>
);
};

View file

@ -1,13 +0,0 @@
import { TicketEdit } from "./_components/TicketEdit";
type PageProps = {
params: Promise<{
id: string;
}>;
};
export default async function Page({ params }: PageProps) {
const { id } = await params;
return <TicketEdit id={id} />;
}

View file

@ -1,11 +0,0 @@
"use client";
import { DisplayError } from "app/_components/DisplayError";
type PageProps = {
error: Error;
};
export default function Page({ error }: PageProps) {
return <DisplayError error={error} />;
}

View file

@ -1,19 +0,0 @@
import { Grid } from "@mui/material";
type LayoutProps = {
detail: any;
edit: any;
};
export default async function Layout({ detail, edit }: LayoutProps) {
return (
<Grid container spacing={0} sx={{ height: "100vh" }} direction="row">
<Grid item sx={{ height: "100vh" }} xs={9}>
{detail}
</Grid>
<Grid item xs={3} sx={{ height: "100vh" }}>
{edit}
</Grid>
</Grid>
);
}

View file

@ -1,15 +0,0 @@
"use client";
import Link from "next/link";
export default function Page() {
return (
<div>
<h2>Not Found</h2>
<p>Could not find requested resource</p>
<p>
View <Link href="/blog">all posts</Link>
</p>
</div>
);
}

View file

@ -10,10 +10,10 @@ const rewriteURL = (
destinationBaseURL: string,
headers: any = {},
) => {
logger.debug({
originBaseURL,
destinationBaseURL,
headerKeys: Object.keys(headers)
logger.debug({
originBaseURL,
destinationBaseURL,
headerKeys: Object.keys(headers)
}, "Rewriting URL");
let path = request.url.replace(originBaseURL, "");
if (path.startsWith("/")) {
@ -58,10 +58,11 @@ const checkRewrites = async (request: NextRequestWithAuth) => {
opensearchBaseURL,
headers,
);
} else {
const isDev = process.env.NODE_ENV === "development";
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
const cspHeader = `
}
const isDev = process.env.NODE_ENV === "development";
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
const cspHeader = `
default-src 'self';
frame-src 'self' https://digiresilience.org;
connect-src 'self';
@ -75,30 +76,29 @@ const checkRewrites = async (request: NextRequestWithAuth) => {
frame-ancestors 'self';
upgrade-insecure-requests;
`;
const contentSecurityPolicyHeaderValue = cspHeader
.replace(/\s{2,}/g, " ")
.trim();
const contentSecurityPolicyHeaderValue = cspHeader
.replace(/\s{2,}/g, " ")
.trim();
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-nonce", nonce);
requestHeaders.set(
"Content-Security-Policy",
contentSecurityPolicyHeaderValue,
);
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-nonce", nonce);
requestHeaders.set(
"Content-Security-Policy",
contentSecurityPolicyHeaderValue,
);
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
response.headers.set(
"Content-Security-Policy",
contentSecurityPolicyHeaderValue,
);
response.headers.set(
"Content-Security-Policy",
contentSecurityPolicyHeaderValue,
);
return response;
};
return response;
}
export default withAuth(checkRewrites, {

View file

@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View file

@ -1,6 +1,6 @@
{
"name": "@link-stack/link",
"version": "3.1.0",
"version": "3.2.0b3",
"type": "module",
"scripts": {
"dev": "next dev -H 0.0.0.0",
@ -10,12 +10,12 @@
"lint": "next lint"
},
"dependencies": {
"@chatscope/chat-ui-kit-react": "^2.0.3",
"@chatscope/chat-ui-kit-react": "^2.1.1",
"@chatscope/chat-ui-kit-styles": "^1.4.0",
"@emotion/cache": "^11.14.0",
"@emotion/react": "^11.14.0",
"@emotion/server": "^11.11.0",
"@emotion/styled": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@link-stack/bridge-common": "*",
"@link-stack/bridge-ui": "*",
"@link-stack/leafcutter-ui": "*",
@ -25,27 +25,27 @@
"@mui/icons-material": "^6",
"@mui/material": "^6",
"@mui/material-nextjs": "^6",
"@mui/x-data-grid-pro": "^7.28.1",
"@mui/x-date-pickers": "^7.28.0",
"@mui/x-date-pickers-pro": "^7.28.0",
"@mui/x-license": "^7.28.0",
"@mui/x-data-grid-pro": "^7",
"@mui/x-date-pickers": "^7",
"@mui/x-date-pickers-pro": "^7",
"@mui/x-license": "^7",
"date-fns": "^4.1.0",
"graphql-request": "^7.1.2",
"ioredis": "^5.6.0",
"graphql-request": "^7.2.0",
"ioredis": "^5.8.1",
"mui-chips-input": "^6.0.0",
"next": "15.2.3",
"next": "15.5.4",
"next-auth": "^4.24.11",
"react": "19.0.0",
"react": "19.2.0",
"react-cookie": "^8.0.1",
"react-dom": "19.0.0",
"react-dom": "19.2.0",
"react-iframe": "^1.8.5",
"react-polyglot": "^0.7.2",
"sharp": "^0.33.5"
"sharp": "^0.34.4"
},
"devDependencies": {
"@link-stack/eslint-config": "*",
"@types/node": "^22.13.12",
"@types/react": "19.0.12",
"@types/uuid": "^10.0.0"
"@types/node": "^24.7.0",
"@types/react": "19.2.2",
"@types/uuid": "^11.0.0"
}
}