Compare commits

..

9 Commits

Author SHA1 Message Date
Henry
f797af64e7 feat: update comments to use tiptap editor 2026-02-08 22:35:42 +00:00
Henry
1f0c780763 feat: skip pending members in sendMentionEmails 2026-02-08 22:15:26 +00:00
Henry
149277ef39 fix: update the editor suggestion to show all members 2026-02-08 22:12:23 +00:00
Henry
c8dcb17510 feat: update card router to send emails on mention 2026-02-07 22:04:31 +00:00
Henry
2f4aea52eb feat: add repo funcs 2026-02-07 22:00:55 +00:00
Henry
7973d8de2f feat: add sendMentionEmail func 2026-02-07 21:58:52 +00:00
Henry
7a0f89c4c0 feat: add mention email template 2026-02-07 21:42:24 +00:00
Henry
105ba45412 chore: gen migration 2026-02-07 21:41:29 +00:00
Henry
3c46d82b7a feat: add notifications schema 2026-02-07 21:41:13 +00:00
22 changed files with 489 additions and 4772 deletions

View File

@@ -20,7 +20,6 @@
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.802.0", "@aws-sdk/client-s3": "^3.802.0",
"@aws-sdk/lib-storage": "^3.802.0",
"@aws-sdk/s3-request-presigner": "^3.812.0", "@aws-sdk/s3-request-presigner": "^3.812.0",
"@headlessui/react": "^2.2.0", "@headlessui/react": "^2.2.0",
"@hookform/resolvers": "^3.3.4", "@hookform/resolvers": "^3.3.4",

View File

@@ -13,23 +13,6 @@ const authHandler = toNodeHandler(auth.handler);
export default withRateLimit( export default withRateLimit(
{ points: 100, duration: 60 }, { points: 100, duration: 60 },
async (req, res) => { async (req, res) => {
/**
* Better-auth behind proxies (Nginx/Cloudflare) can sometimes fail to parse the protocol
* if headers are incorrectly set or if there are multiple values in X-Forwarded-Proto.
* We sanitize these headers here to ensure better-auth gets a clean protocol and host.
*/
const forwardedProto = req.headers["x-forwarded-proto"];
if (forwardedProto) {
const p = Array.isArray(forwardedProto) ? forwardedProto[0] : forwardedProto;
req.headers["x-forwarded-proto"] = p?.split(",")[0]?.trim();
}
const forwardedHost = req.headers["x-forwarded-host"];
if (forwardedHost) {
const h = Array.isArray(forwardedHost) ? forwardedHost[0] : forwardedHost;
req.headers["host"] = h?.split(",")[0]?.trim();
}
return await authHandler(req, res); return await authHandler(req, res);
}, },
); );

View File

@@ -1,17 +1,17 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { Upload } from "@aws-sdk/lib-storage"; import { PutObjectCommand } from "@aws-sdk/client-s3";
import { createNextApiContext } from "@kan/api/trpc"; import { createNextApiContext } from "@kan/api/trpc";
import { assertPermission } from "@kan/api/utils/permissions";
import { withRateLimit } from "@kan/api/utils/rateLimit";
import * as cardRepo from "@kan/db/repository/card.repo"; import * as cardRepo from "@kan/db/repository/card.repo";
import * as cardActivityRepo from "@kan/db/repository/cardActivity.repo"; import * as cardActivityRepo from "@kan/db/repository/cardActivity.repo";
import * as cardAttachmentRepo from "@kan/db/repository/cardAttachment.repo"; import * as cardAttachmentRepo from "@kan/db/repository/cardAttachment.repo";
import { createS3Client, generateUID } from "@kan/shared/utils"; import { generateUID } from "@kan/shared/utils";
import { env } from "~/env"; import { env } from "~/env";
import { withRateLimit } from "@kan/api/utils/rateLimit";
import { createS3Client } from "@kan/shared/utils";
import { assertPermission } from "@kan/api/utils/permissions";
// FIXME: Respect the environment variable: NEXT_API_BODY_SIZE_LIMIT
const MAX_SIZE_BYTES = 50 * 1024 * 1024; // 50MB const MAX_SIZE_BYTES = 50 * 1024 * 1024; // 50MB
export const config = { export const config = {
@@ -90,19 +90,16 @@ export default withRateLimit(
const client = createS3Client(); const client = createS3Client();
const upload = new Upload({ // Upload the file to S3
client, await client.send(
params: { new PutObjectCommand({
Bucket: bucket, Bucket: bucket,
Key: s3Key, Key: s3Key,
Body: req, Body: req,
ContentType: contentType, ContentType: contentType,
ContentLength: contentLength, ContentLength: contentLength,
}, }),
leavePartsOnError: false, );
});
await upload.done();
// Create attachment record and log activity // Create attachment record and log activity
const attachment = await cardAttachmentRepo.create(db, { const attachment = await cardAttachmentRepo.create(db, {
@@ -115,15 +112,9 @@ export default withRateLimit(
createdBy: user.id, createdBy: user.id,
}); });
if (!attachment) {
return res.status(500).json({ error: "Failed to create attachment" });
}
await cardActivityRepo.create(db, { await cardActivityRepo.create(db, {
type: "card.updated.attachment.added", type: "card.updated.attachment.added",
cardId: card.id, cardId: card.id,
attachmentId: attachment.id,
toTitle: originalFilenameHeader,
createdBy: user.id, createdBy: user.id,
}); });
@@ -134,3 +125,4 @@ export default withRateLimit(
} }
}, },
); );

View File

@@ -66,8 +66,8 @@ const Card = ({
const hasDueDate = !!dueDate; const hasDueDate = !!dueDate;
return ( return (
<div className="flex flex-col overflow-hidden rounded-md border border-light-200 bg-light-50 px-3 py-2 text-sm text-neutral-900 dark:border-dark-200 dark:bg-dark-200 dark:text-dark-1000 dark:hover:bg-dark-300"> <div className="flex flex-col rounded-md border border-light-200 bg-light-50 px-3 py-2 text-sm text-neutral-900 dark:border-dark-200 dark:bg-dark-200 dark:text-dark-1000 dark:hover:bg-dark-300">
<span className="break-words">{title}</span> <span>{title}</span>
{labels.length || {labels.length ||
members.length || members.length ||
checklists.length > 0 || checklists.length > 0 ||

View File

@@ -58,14 +58,13 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
const [selectedPublicListId, setSelectedPublicListId] = const [selectedPublicListId, setSelectedPublicListId] =
useState<PublicListId>(""); useState<PublicListId>("");
const [isInitialLoading, setIsInitialLoading] = useState(true); const [isInitialLoading, setIsInitialLoading] = useState(true);
const { ref: scrollRef, onMouseDown } = useDragToScroll({ const { ref: scrollRef, onMouseDown } = useDragToScroll({
enabled: true, enabled: true,
direction: "horizontal", direction: "horizontal",
}); });
const { canCreateList, canEditList, canEditCard, canEditBoard } = const { canCreateList, canEditList, canEditCard, canEditBoard } = usePermissions();
usePermissions();
const { tooltipContent: createListShortcutTooltipContent } = const { tooltipContent: createListShortcutTooltipContent } =
useKeyboardShortcut({ useKeyboardShortcut({
@@ -418,9 +417,10 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
{...register("name")} {...register("name")}
onBlur={canEditBoard ? handleSubmit(onSubmit) : undefined} onBlur={canEditBoard ? handleSubmit(onSubmit) : undefined}
readOnly={!canEditBoard} readOnly={!canEditBoard}
className="block border-0 bg-transparent p-0 py-0 font-bold leading-[2.3rem] tracking-tight text-neutral-900 focus:ring-0 focus-visible:outline-none disabled:cursor-not-allowed dark:text-dark-1000 sm:text-[1.2rem]" className="block border-0 bg-transparent p-0 py-0 font-bold leading-[2.3rem] tracking-tight text-neutral-900 focus:ring-0 focus-visible:outline-none dark:text-dark-1000 sm:text-[1.2rem] disabled:cursor-not-allowed"
/> />
</form> </form>
)} )}
{!boardData && !isLoading && ( {!boardData && !isLoading && (
<p className="order-2 block p-0 py-0 font-bold leading-[2.3rem] tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem] md:order-1"> <p className="order-2 block p-0 py-0 font-bold leading-[2.3rem] tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem] md:order-1">
@@ -598,13 +598,12 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
? `/templates/${boardId}/cards/${card.publicId}` ? `/templates/${boardId}/cards/${card.publicId}`
: `/cards/${card.publicId}` : `/cards/${card.publicId}`
} }
className={`mb-2 flex !cursor-pointer flex-col ${ className={`mb-2 flex !cursor-pointer flex-col ${card.publicId.startsWith(
card.publicId.startsWith( "PLACEHOLDER",
"PLACEHOLDER", )
) ? "pointer-events-none"
? "pointer-events-none" : ""
: "" }`}
}`}
ref={provided.innerRef} ref={provided.innerRef}
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps} {...provided.dragHandleProps}

View File

@@ -23,18 +23,6 @@ export const getTemplates = (): Template[] => [
lists: [t`Backlog`, t`To Do`, t`In Progress`, t`Code Review`, t`Done`], lists: [t`Backlog`, t`To Do`, t`In Progress`, t`Code Review`, t`Done`],
labels: [t`Bug`, t`Feature`, t`Enhancement`, t`Critical`, t`Documentation`], labels: [t`Bug`, t`Feature`, t`Enhancement`, t`Critical`, t`Documentation`],
}, },
{
id: "roadmap-basic",
name: t`Basic Roadmap`,
lists: [t`Requested`, t`Planned`, t`In Progress`, t`Done`],
labels: [t`Feature`, t`Enhancement`, t`Critical`, t`Documentation`],
},
{
id: "roadmap-extended",
name: t`Extended Roadmap`,
lists: [t`Requested`, t`Under Review`, t`Planned`, t`In Progress`, t`Done`, t`Rejected`],
labels: [t`Feature`, t`Enhancement`, t`Critical`, t`Documentation`],
},
{ {
id: "content-creation", id: "content-creation",
name: t`Content Creation`, name: t`Content Creation`,

View File

@@ -8,7 +8,6 @@ import {
HiOutlineArrowRight, HiOutlineArrowRight,
HiOutlineCheckCircle, HiOutlineCheckCircle,
HiOutlineClock, HiOutlineClock,
HiOutlinePaperClip,
HiOutlinePencil, HiOutlinePencil,
HiOutlinePlus, HiOutlinePlus,
HiOutlineTag, HiOutlineTag,
@@ -32,16 +31,6 @@ import Comment from "./Comment";
type ActivityType = type ActivityType =
NonNullable<GetCardByIdOutput>["activities"][number]["type"]; NonNullable<GetCardByIdOutput>["activities"][number]["type"];
type ActivityWithMergedLabels =
GetCardActivitiesOutput["activities"][number] & {
mergedLabels?: string[];
attachment?: {
publicId: string;
filename: string;
originalFilename: string;
} | null;
};
const truncate = (value: string | null, maxLength = 50) => { const truncate = (value: string | null, maxLength = 50) => {
if (!value) return value; if (!value) return value;
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}` : value; return value.length > maxLength ? `${value.slice(0, maxLength - 1)}` : value;
@@ -68,7 +57,6 @@ const getActivityText = ({
toDueDate, toDueDate,
dateLocale, dateLocale,
mergedLabels, mergedLabels,
attachmentName,
}: { }: {
type: ActivityType; type: ActivityType;
toTitle: string | null; toTitle: string | null;
@@ -83,7 +71,6 @@ const getActivityText = ({
toDueDate?: Date | null; toDueDate?: Date | null;
dateLocale: DateFnsLocale; dateLocale: DateFnsLocale;
mergedLabels?: string[]; mergedLabels?: string[];
attachmentName?: string | null;
}) => { }) => {
const displayName = memberName ?? memberEmail ?? t`Member`; const displayName = memberName ?? memberEmail ?? t`Member`;
const TextHighlight = ({ children }: { children: React.ReactNode }) => ( const TextHighlight = ({ children }: { children: React.ReactNode }) => (
@@ -137,8 +124,6 @@ const getActivityText = ({
"card.updated.checklist.item.completed": t`completed a checklist item`, "card.updated.checklist.item.completed": t`completed a checklist item`,
"card.updated.checklist.item.uncompleted": t`marked a checklist item as incomplete`, "card.updated.checklist.item.uncompleted": t`marked a checklist item as incomplete`,
"card.updated.checklist.item.deleted": t`deleted a checklist item`, "card.updated.checklist.item.deleted": t`deleted a checklist item`,
"card.updated.attachment.added": t`added an attachment`,
"card.updated.attachment.removed": t`removed an attachment`,
"card.updated.dueDate.added": t`set the due date`, "card.updated.dueDate.added": t`set the due date`,
"card.updated.dueDate.updated": t`updated the due date`, "card.updated.dueDate.updated": t`updated the due date`,
"card.updated.dueDate.removed": t`removed the due date`, "card.updated.dueDate.removed": t`removed the due date`,
@@ -271,27 +256,6 @@ const getActivityText = ({
); );
} }
if (type === "card.updated.attachment.added") {
const filename = attachmentName ?? toTitle;
if (!filename) return baseText;
return (
<Trans>
added an attachment <TextHighlight>{truncate(filename)}</TextHighlight>
</Trans>
);
}
if (type === "card.updated.attachment.removed") {
const filename = attachmentName ?? fromTitle;
if (!filename) return baseText;
return (
<Trans>
removed an attachment{" "}
<TextHighlight>{truncate(filename)}</TextHighlight>
</Trans>
);
}
if (type === "card.updated.dueDate.added" && toDueDate) { if (type === "card.updated.dueDate.added" && toDueDate) {
const showYear = !isSameYear(toDueDate, new Date()); const showYear = !isSameYear(toDueDate, new Date());
const formattedDate = format( const formattedDate = format(
@@ -344,8 +308,6 @@ const ACTIVITY_ICON_MAP: Partial<Record<ActivityType, React.ReactNode | null>> =
"card.updated.checklist.item.completed": <HiOutlineCheckCircle />, "card.updated.checklist.item.completed": <HiOutlineCheckCircle />,
"card.updated.checklist.item.uncompleted": <HiOutlineCheckCircle />, "card.updated.checklist.item.uncompleted": <HiOutlineCheckCircle />,
"card.updated.checklist.item.deleted": <HiOutlineTrash />, "card.updated.checklist.item.deleted": <HiOutlineTrash />,
"card.updated.attachment.added": <HiOutlinePaperClip />,
"card.updated.attachment.removed": <HiOutlinePaperClip />,
"card.updated.dueDate.added": <HiOutlineClock />, "card.updated.dueDate.added": <HiOutlineClock />,
"card.updated.dueDate.updated": <HiOutlineClock />, "card.updated.dueDate.updated": <HiOutlineClock />,
"card.updated.dueDate.removed": <HiOutlineClock />, "card.updated.dueDate.removed": <HiOutlineClock />,
@@ -379,7 +341,7 @@ const ActivityList = ({
isAdmin?: boolean; isAdmin?: boolean;
isViewOnly?: boolean; isViewOnly?: boolean;
}) => { }) => {
const { dateLocale } = useLocalisation(); const { dateLocale, locale } = useLocalisation();
const { data: sessionData } = authClient.useSession(); const { data: sessionData } = authClient.useSession();
const utils = api.useUtils(); const utils = api.useUtils();
const [allActivities, setAllActivities] = useState< const [allActivities, setAllActivities] = useState<
@@ -429,21 +391,25 @@ const ActivityList = ({
cursor: nextCursor, cursor: nextCursor,
}); });
const existingIds = new Set( if (nextPage) {
currentActivities.map((a) => a.publicId), const existingIds = new Set(
); currentActivities.map((a) => a.publicId),
const newActivities = nextPage.activities.filter( );
(a: { publicId: string }) => !existingIds.has(a.publicId), const newActivities = nextPage.activities.filter(
); (a: { publicId: string }) => !existingIds.has(a.publicId),
currentActivities = [...currentActivities, ...newActivities]; );
currentHasMore = nextPage.hasMore; currentActivities = [...currentActivities, ...newActivities];
currentHasMore = nextPage.hasMore;
} else {
break;
}
} }
setAllActivities(currentActivities); setAllActivities(currentActivities);
setHasMore(false); setHasMore(false);
}; };
void fetchAllRemaining(); fetchAllRemaining();
} else { } else {
setAllActivities(firstPageData.activities); setAllActivities(firstPageData.activities);
setHasMore(firstPageData.hasMore); setHasMore(firstPageData.hasMore);
@@ -470,15 +436,17 @@ const ActivityList = ({
cursor: nextCursor, cursor: nextCursor,
}); });
const existingIds = new Set(allActivities.map((a) => a.publicId)); if (nextPage) {
const newActivities = nextPage.activities.filter( const existingIds = new Set(allActivities.map((a) => a.publicId));
(a: { publicId: string }) => !existingIds.has(a.publicId), const newActivities = nextPage.activities.filter(
); (a: { publicId: string }) => !existingIds.has(a.publicId),
setAllActivities((prev) => [...prev, ...newActivities]); );
setHasMore(nextPage.hasMore); setAllActivities((prev) => [...prev, ...newActivities]);
setHasMore(nextPage.hasMore);
if (!nextPage.hasMore) { if (!nextPage.hasMore) {
isFullyExpandedRef.current = true; isFullyExpandedRef.current = true;
}
} }
} finally { } finally {
setIsLoadingMore(false); setIsLoadingMore(false);
@@ -505,10 +473,7 @@ const ActivityList = ({
fromDueDate: activity.fromDueDate ?? null, fromDueDate: activity.fromDueDate ?? null,
toDueDate: activity.toDueDate ?? null, toDueDate: activity.toDueDate ?? null,
dateLocale: dateLocale, dateLocale: dateLocale,
mergedLabels: (activity as ActivityWithMergedLabels).mergedLabels, mergedLabels: (activity as any).mergedLabels,
attachmentName:
(activity as ActivityWithMergedLabels).attachment?.originalFilename ??
null,
}); });
if (activity.type === "card.updated.comment.added") if (activity.type === "card.updated.comment.added")

View File

@@ -4,7 +4,6 @@ import { t } from "@lingui/core/macro";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { IoChevronForwardSharp } from "react-icons/io5"; import { IoChevronForwardSharp } from "react-icons/io5";
import { HiXMark } from "react-icons/hi2";
import { authClient } from "@kan/auth/client"; import { authClient } from "@kan/auth/client";
@@ -340,13 +339,6 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
boardPublicId={boardId} boardPublicId={boardId}
cardCreatedBy={card?.createdBy} cardCreatedBy={card?.createdBy}
/> />
<Link
href={`/${isTemplate ? "templates" : "boards"}/${boardId}`}
className="flex h-7 w-7 items-center justify-center rounded-[5px] text-light-900 hover:bg-light-200 dark:text-dark-900 dark:hover:bg-dark-200"
aria-label={t`Close`}
>
<HiXMark className="h-5 w-5" />
</Link>
</div> </div>
</> </>
)} )}

View File

@@ -1,587 +1,293 @@
import { t } from "@lingui/core/macro"; import { t } from "@lingui/core/macro";
import React, { useEffect, useRef, useState } from "react";
import { HiCheckCircle, HiXCircle } from "react-icons/hi2"; import { HiCheckCircle, HiXCircle } from "react-icons/hi2";
import { twMerge } from "tailwind-merge";
interface Feature {
key: string;
label: string;
kan: boolean | string;
trello: boolean | string;
}
interface Section {
name: string;
features: Feature[];
}
type FrequencyValue = "monthly" | "annually"; type FrequencyValue = "monthly" | "annually";
interface PlanFeature { const CellValue = ({ value }: { value: boolean | string }) => {
key: string; if (typeof value === "string") {
label: string; return (
free: string | boolean | { text: string; highlight?: string }; <span className="text-sm text-dark-900 dark:text-dark-900">{value}</span>
teams: string | boolean | { text: string; highlight?: string }; );
pro: string | boolean | { text: string; highlight?: string }; }
enterprise: string | boolean | { text: string; highlight?: string }; return value ? (
} <HiCheckCircle className="h-5 w-5 text-light-950 dark:text-dark-1000" />
) : (
interface FeatureSection { <HiXCircle className="h-5 w-5 text-light-700 dark:text-dark-600" />
name: string; );
features: PlanFeature[]; };
}
const FeatureComparisonTable = ({ const FeatureComparisonTable = ({
frequencyValue: _frequencyValue, frequencyValue,
}: { }: {
frequencyValue: FrequencyValue; frequencyValue: FrequencyValue;
}) => { }) => {
// Keep pricing column headers visible as you scroll const sections: Section[] = [
const containerRef = useRef<HTMLDivElement | null>(null);
const [isHeaderFixed, setIsHeaderFixed] = useState(false);
const [headerRect, setHeaderRect] = useState<{ left: number; width: number }>({
left: 0,
width: 0,
});
useEffect(() => {
const HEADER_OFFSET = 64; // px; matches fixed top nav height
const handleScroll = () => {
if (!containerRef.current) return;
// Hide fixed header on mobile (screens smaller than 620px)
if (window.innerWidth < 620) {
setIsHeaderFixed(false);
return;
}
const rect = containerRef.current.getBoundingClientRect();
const pastHeader = rect.top <= HEADER_OFFSET;
const aboveBottom = rect.bottom > HEADER_OFFSET + 40;
const shouldFix = pastHeader && aboveBottom;
setIsHeaderFixed(shouldFix);
if (shouldFix) {
setHeaderRect({
left: rect.left,
width: rect.width,
});
}
};
handleScroll();
window.addEventListener("scroll", handleScroll, { passive: true });
window.addEventListener("resize", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
window.removeEventListener("resize", handleScroll);
};
}, []);
const planSpecificLimits: PlanFeature[] = [
{
key: "boards",
label: t`Boards`,
free: { text: t`Unlimited boards`, highlight: "Unlimited" },
teams: { text: t`Unlimited boards`, highlight: "Unlimited" },
pro: { text: t`Unlimited boards`, highlight: "Unlimited" },
enterprise: { text: t`Unlimited boards`, highlight: "Unlimited" },
},
{
key: "members",
label: t`Members`,
free: { text: t`1 user`, highlight: "1" },
teams: { text: t`Per-seat pricing`, highlight: "Per-seat" },
pro: { text: t`Unlimited members`, highlight: "Unlimited" },
enterprise: { text: t`Unlimited members`, highlight: "Unlimited" },
},
{
key: "file-uploads",
label: t`File uploads`,
free: { text: t`10mb file uploads`, highlight: "10mb" },
teams: { text: t`Unlimited file uploads`, highlight: "Unlimited" },
pro: { text: t`Unlimited file uploads`, highlight: "Unlimited" },
enterprise: { text: t`Unlimited file uploads`, highlight: "Unlimited" },
},
{
key: "workspace-username",
label: t`Workspace username`,
free: { text: t`Default username`, highlight: "Default" },
teams: { text: t`Default username`, highlight: "Default" },
pro: { text: t`Custom username`, highlight: "Custom" },
enterprise: { text: t`Custom username`, highlight: "Custom" },
},
];
const coreFeatures: PlanFeature[] = [
{
key: "checklists",
label: t`Checklists`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
{
key: "custom-templates",
label: t`Board templates`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
{
key: "labels-filters",
label: t`Labels & filters`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
{
key: "board-visibility",
label: t`Board visibility`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
{
key: "search",
label: t`Intelligent search`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
{
key: "activity-log",
label: t`Activity log`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
{
key: "comments",
label: t`Comments`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
{
key: "roles-permissions",
label: t`Roles & permissions`,
free: false,
teams: false,
pro: true,
enterprise: true,
},
{
key: "custom-branding",
label: t`Custom branding`,
free: false,
teams: false,
pro: true,
enterprise: true,
},
];
const collaborationFeatures: PlanFeature[] = [
{
key: "mentions",
label: t`Mentions`,
free: false,
teams: true,
pro: true,
enterprise: true,
},
{
key: "email-notifications",
label: t`Email notifications`,
free: false,
teams: true,
pro: true,
enterprise: true,
},
{
key: "assignees",
label: t`Assignees`,
free: false,
teams: true,
pro: true,
enterprise: true,
},
{
key: "invite-links",
label: t`Invite links`,
free: false,
teams: true,
pro: true,
enterprise: true,
},
];
const importsFeatures: PlanFeature[] = [
{
key: "import-export",
label: t`Trello`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
];
const platformFeatures: PlanFeature[] = [
{
key: "open-source",
label: t`Open source`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
{
key: "rest-api",
label: t`API`,
free: { text: t`Limited API access` },
teams: { text: t`Full API access` },
pro: { text: t`Full API access` },
enterprise: { text: t`Full API access` },
},
{
key: "self-hostable",
label: t`On-premise`,
free: false,
teams: false,
pro: false,
enterprise: true,
},
];
const securityFeatures: PlanFeature[] = [
{
key: "sso",
label: t`SSO`,
free: { text: t`Google SSO` },
teams: { text: t`Google SSO` },
pro: { text: t`Google SSO` },
enterprise: { text: t`Google SSO + SAML` },
},
{
key: "admin-roles",
label: t`Admin roles`,
free: { text: t`Admin roles` },
teams: { text: t`Admin roles` },
pro: { text: t`Admin roles` },
enterprise: { text: t`Advanced admin roles` },
},
];
const supportFeatures: PlanFeature[] = [
{
key: "priority-email-support",
label: t`Priority email support`,
free: false,
teams: true,
pro: true,
enterprise: true,
},
{
key: "account-manager",
label: t`Account manager`,
free: false,
teams: false,
pro: false,
enterprise: true,
},
{
key: "sla",
label: t`SLA`,
free: false,
teams: false,
pro: false,
enterprise: true,
},
];
const sections: FeatureSection[] = [
{
name: "",
features: planSpecificLimits,
},
{ {
name: t`Core features`, name: t`Core features`,
features: coreFeatures, features: [
{
key: "ulimited-workspaces",
label: t`Unlimited workspaces`,
kan: true,
trello: false,
},
{
key: "unlimited-boards",
label: t`Unlimited boards`,
kan: true,
trello: false,
},
{
key: "unlimited-lists",
label: t`Unlimited lists`,
kan: true,
trello: true,
},
{
key: "unlimited-cards",
label: t`Unlimited cards`,
kan: true,
trello: true,
},
{
key: "activity-log",
label: t`Activity log`,
kan: true,
trello: true,
},
{
key: "templates",
label: t`Custom templates`,
kan: true,
trello: false,
},
{ key: "labels", label: t`Labels & filters`, kan: true, trello: true },
{ key: "checklists", label: t`Checklists`, kan: true, trello: true },
{
key: "import",
label: t`Import from Trello`,
kan: true,
trello: false,
},
{
key: "visibility",
label: t`Board visibility`,
kan: true,
trello: true,
},
{
key: "search",
label: t`Intelligent search`,
kan: true,
trello: true,
},
{
key: "workspace-url",
label: t`Custom workspace link`,
kan: true,
trello: false,
},
],
}, },
{ {
name: t`Collaboration`, name: t`Teams`,
features: collaborationFeatures, features: [
}, {
{ key: "pricing",
name: t`Imports`, label: t`Price per user/month`,
features: importsFeatures, kan: frequencyValue === "monthly" ? `$10.00` : `$8.00`,
trello: frequencyValue === "monthly" ? `$12.00` : `$10.00`,
},
{
key: "comments",
label: t`Comments & mentions`,
kan: true,
trello: true,
},
{
key: "assignments",
label: t`Assignees`,
kan: true,
trello: true,
},
{
key: "sharing",
label: t`Board sharing & invites`,
kan: true,
trello: true,
},
{
key: "invite-links",
label: t`Invite links`,
kan: true,
trello: true,
},
],
}, },
{ {
name: t`Platform`, name: t`Platform`,
features: platformFeatures, features: [
}, {
{ key: "api",
name: t`Security`, label: t`REST API`,
features: securityFeatures, kan: true,
}, trello: true,
{ },
name: t`Support`, {
features: supportFeatures, key: "open-source",
label: t`Open source`,
kan: true,
trello: false,
},
{
key: "self-hostable",
label: t`Self-hostable`,
kan: true,
trello: false,
},
{
key: "white-label",
label: t`White label`,
kan: true,
trello: false,
},
{
key: "performance",
label: t`Fast & lightweight`,
kan: true,
trello: false,
},
{
key: "this-is-a-joke",
label: t`Owned by Atlassian`,
kan: false,
trello: true,
},
],
}, },
]; ];
const plans = [ const products = [
{ id: "free", name: t`Free`, tierId: "tier-free" }, {
{ id: "teams", name: t`Teams`, tierId: "tier-teams" }, id: "kan",
{ id: "pro", name: t`Pro`, tierId: "tier-pro" }, name: t`Kan`,
{ id: "enterprise", name: t`Enterprise`, tierId: "tier-enterprise" }, featured: true,
},
{
id: "trello",
name: t`Trello`,
featured: false,
},
]; ];
const isHighlightValue = (
value: string | boolean | { text: string; highlight?: string },
): value is { text: string; highlight?: string } => {
return typeof value === "object" && value !== null && "text" in value;
};
const shouldHighlight = (text: string): boolean => {
const lowerText = text.toLowerCase();
return ["unlimited", "multiple", "per-seat", "custom", "mentions", "full", "sso", "admin"].some(
(keyword) => lowerText.includes(keyword),
);
};
const CellValue = ({
value,
label,
}: {
value: string | boolean | { text: string; highlight?: string };
label: string;
}) => {
// Handle object with text and highlight
if (isHighlightValue(value)) {
const { text, highlight } = value;
const isUnlimited = shouldHighlight(text);
let displayText;
if (highlight) {
const parts = text.split(new RegExp(`(${highlight})`, "gi"));
displayText = (
<>
{parts.map((part, index) =>
part.toLowerCase() === highlight.toLowerCase() ? (
<span key={index} className="text-light-1000 dark:text-dark-1000">
{part}
</span>
) : (
<span key={index} className="text-light-950 dark:text-dark-800">
{part}
</span>
),
)}
</>
);
} else {
displayText = text;
}
return (
<div className="flex items-center gap-2.5">
<HiCheckCircle
className={twMerge(
"mt-0.5 h-4 w-4 shrink-0",
isUnlimited
? "text-light-1000 dark:text-dark-1000"
: "text-light-400 dark:text-dark-600",
)}
/>
<span
className={twMerge(
"text-sm font-medium",
isUnlimited
? "text-light-1000 dark:text-dark-950"
: "text-light-950 dark:text-dark-800",
)}
>
{displayText}
</span>
</div>
);
}
if (typeof value === "string") {
const isUnlimited = shouldHighlight(value);
return (
<div className="flex items-center gap-2.5">
<HiCheckCircle
className={twMerge(
"mt-0.5 h-4 w-4 shrink-0",
isUnlimited
? "text-light-1000 dark:text-dark-1000"
: "text-light-400 dark:text-dark-600",
)}
/>
<span
className={twMerge(
"text-sm font-medium",
isUnlimited
? "text-light-1000 dark:text-dark-950"
: "text-light-950 dark:text-dark-800",
)}
>
{value}
</span>
</div>
);
}
if (value) {
// Boolean true - show checkmark icon
return (
<div className="flex items-center gap-2.5">
<HiCheckCircle className="mt-0.5 h-4 w-4 shrink-0 text-light-1000 dark:text-dark-1000" />
<span className="text-sm font-medium text-light-1000 dark:text-dark-950">
{label}
</span>
</div>
);
}
// Boolean false - show cross icon
return (
<div className="flex items-center gap-2.5">
<HiXCircle className="mt-0.5 h-4 w-4 shrink-0 text-light-400 dark:text-dark-600" />
<span className="text-sm font-medium text-light-800 dark:text-dark-800">
{label}
</span>
</div>
);
};
const getFeatureValue = (
feature: PlanFeature,
planId: string,
): string | boolean | { text: string; highlight?: string } => {
switch (planId) {
case "free":
return feature.free;
case "teams":
return feature.teams;
case "pro":
return feature.pro;
case "enterprise":
return feature.enterprise;
default:
return false;
}
};
return ( return (
<section aria-labelledby="comparison-heading" className="w-full"> <section aria-labelledby="comparison-heading" className="w-full px-4">
<div className="mx-auto max-w-7xl"> <div className="mx-auto max-w-6xl py-8 lg:py-10">
<div <div className="grid grid-cols-3 gap-x-8 border-t border-light-500 before:block dark:border-dark-300">
ref={containerRef} {products.map((product) => (
className="relative overflow-x-auto rounded-lg border border-light-300 bg-light-50 dark:border-dark-300 dark:bg-dark-50" <div key={product.id} aria-hidden="true" className="-mt-px">
> <div
{isHeaderFixed && ( className={`${product.featured ? "border-dark-200 dark:border-dark-800" : "border-transparent"} border-t-2 pt-8`}
<div >
className="hidden sm:block fixed z-40 border-b border-light-300 bg-light-50/95 dark:border-dark-400 dark:bg-dark-50/95" <p
style={{ className={`${product.featured ? "text-dark-50 dark:text-dark-1000" : "text-light-1000 dark:text-dark-1000"} text-center text-sm font-semibold`}
top: 64, >
left: headerRect.left, {product.name}
width: headerRect.width, </p>
}}
>
<div className="grid grid-cols-4 border-x border-light-300 dark:border-dark-300">
{plans.map((plan) => {
const isMostPopular = plan.id === "pro";
return (
<div
key={`fixed-${plan.id}`}
className={twMerge(
"flex items-center px-6 py-3 text-left text-base font-semibold",
isMostPopular
? "bg-light-200 text-light-1000 dark:bg-dark-100 dark:text-dark-1000"
: "bg-light-50 text-dark-50 dark:bg-dark-50 dark:text-dark-1000",
)}
>
{plan.name}
{plan.id === "pro" && <span className="ml-1 text-xl"></span>}
</div>
);
})}
</div> </div>
</div> </div>
)} ))}
<table className="w-full border-separate border-spacing-0"> </div>
<thead>
<tr> <div className="-mt-6 space-y-12">
{plans.map((plan) => { {sections.map((section) => (
const isMostPopular = plan.id === "pro"; <div key={section.name}>
return ( <h4 className="text-sm font-semibold text-light-1000 dark:text-dark-1000">
<th {section.name}
key={plan.id} </h4>
scope="col" <div className="relative -mx-8 mt-8">
className={twMerge( <div
"w-1/4 px-6 py-4 text-left text-base font-semibold", aria-hidden="true"
isMostPopular className="absolute inset-x-8 inset-y-0 grid grid-cols-3 gap-x-8 before:block"
? "bg-light-200 text-light-1000 dark:bg-dark-100 dark:text-dark-1000" >
: "bg-light-50 text-dark-50 dark:bg-dark-50 dark:text-dark-1000", {products.map((product) => (
)} <div
> key={product.id}
<span className="flex items-center"> className={`size-full rounded-lg ${product.featured ? "bg-white shadow-sm dark:bg-dark-50 dark:shadow-none" : ""}`}
{plan.name} />
{plan.id === "pro" && <span className="ml-1 text-xl"></span>}
</span>
</th>
);
})}
</tr>
</thead>
<tbody>
{sections.map((section, sectionIdx) => (
<React.Fragment key={section.name || `section-${sectionIdx}`}>
{section.name && (
<tr>
{plans.map((plan) => {
const isPro = plan.id === "pro";
return (
<td
key={plan.id}
className={twMerge(
"border-t px-6 py-3 text-left text-sm font-semibold text-dark-900 dark:text-dark-900",
isPro
? "border-light-400 bg-light-200 dark:border-dark-400 dark:bg-dark-100"
: "border-light-300 bg-light-50 dark:border-dark-400 dark:bg-dark-50",
)}
>
{plan.id === "free" ? section.name : ""}
</td>
);
})}
</tr>
)}
{section.features.map((feature) => (
<tr key={feature.key}>
{plans.map((plan) => {
const isPro = plan.id === "pro";
return (
<td
key={plan.id}
className={twMerge(
"w-1/4 border-t px-6 py-3 text-left",
isPro
? "border-light-400 bg-light-200 dark:border-dark-400 dark:bg-dark-100"
: "border-light-300 dark:border-dark-400",
)}
>
<CellValue
value={getFeatureValue(feature, plan.id)}
label={feature.label}
/>
</td>
);
})}
</tr>
))} ))}
</React.Fragment> </div>
))}
</tbody> <table className="relative w-full border-separate border-spacing-x-8">
</table> <thead>
<tr className="text-left">
<th scope="col">
<span className="sr-only">{t`Feature`}</span>
</th>
{products.map((p) => (
<th key={p.id} scope="col">
<span className="sr-only">{p.name}</span>
</th>
))}
</tr>
</thead>
<tbody>
{section.features.map((feature, featureIdx) => (
<tr key={feature.key}>
<th
scope="row"
className="w-1/3 py-3 pr-4 text-left text-sm font-normal text-light-1000 dark:text-dark-1000"
>
{feature.label}
{featureIdx !== section.features.length - 1 ? (
<div className="absolute inset-x-8 mt-3 h-px bg-light-500 dark:bg-dark-300" />
) : null}
</th>
{products.map((p) => (
<td
key={p.id}
className="relative w-1/3 px-4 py-0 text-center"
>
<span className="relative inline-flex w-full items-center justify-center py-3">
<CellValue
value={
p.id === "kan" ? feature.kan : feature.trello
}
/>
</span>
</td>
))}
</tr>
))}
</tbody>
</table>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-8 inset-y-0 grid grid-cols-3 gap-x-8 before:block"
>
{products.map((product) => (
<div
key={product.id}
className={`${product.featured ? "ring-1 ring-light-500 dark:ring-dark-800" : "ring-0"} rounded-2xl`}
/>
))}
</div>
</div>
</div>
))}
</div> </div>
</div> </div>
</section> </section>

View File

@@ -23,115 +23,81 @@ const Pricing = ({
}) => { }) => {
const tiers = [ const tiers = [
{ {
name: t`Free`, name: t`Individuals`,
id: "tier-free", id: "tier-individuals",
href: "signup", href: "signup",
buttonText: t`Get started`, buttonText: t`Get Started`,
price: { monthly: t`$0`, annually: t`$0` }, price: { monthly: t`Free`, annually: t`Free` },
description: t`Free for everyone`, description: t`Everything you need, free forever. Unlimited boards, unlimited lists, unlimited cards. Upgrade any time.`,
bestFor: t`Best for individuals and solo creators getting started`, featureHeader: t`Free, forever`,
featureHeader: undefined,
features: [ features: [
{ text: t`1 user` }, t`1 user`,
{ text: t`Unlimited boards` }, t`Unlimited boards`,
{ text: t`Unlimited cards` }, t`Unlimited cards`,
{ text: t`Custom board templates` }, t`Custom board templates`,
{ text: t`Checklists` }, t`Checklists`,
], ],
showPrice: true, showPrice: true,
ctaSubtext: undefined,
mostPopular: false,
highlighted: false,
}, },
{ {
name: t`Teams`, name: t`Teams`,
id: "tier-teams", id: "tier-teams",
href: "signup", href: "signup",
buttonText: t`Get started`, buttonText: t`Get Started`,
price: { monthly: "$10", annually: "$8" }, price: { monthly: "$10.00", annually: "$8.00" },
description: t`Perfect for small and growing teams looking to collaborate.`, description: t`Kan is better with a team. Perfect for small and growing teams looking to collaborate.`,
bestFor: t`Best for small and growing teams that need collaboration`, featureHeader: t`Everything in the free plan, plus:`,
featureHeader: undefined,
features: [ features: [
{ text: t`All Free features +` }, t`Workspace members`,
{ text: t`Workspace members` }, t`Admin roles`,
{ text: t`Invite links` }, t`Priority email support`,
{ text: t`Unlimited file uploads` }, t`Support the development of the project`,
{ text: t`Email notifications for mentions` },
{ text: t`Priority email support` },
],
highlighted: false,
showPrice: true,
showPriceSuffix: true,
ctaSubtext: t`14 day free trial · Switch plans or cancel anytime`,
mostPopular: false,
},
{
name: t`Pro`,
id: "tier-pro",
href: "signup",
buttonText: t`Get started`,
price: { monthly: "$29", annually: "$24" },
description: t`Unlimited seats and advanced features for growing teams.`,
bestFor: t`Best for teams that want to scale without limits`,
featureHeader: undefined,
features: [
{ text: t`All Teams features +` },
{ text: t`Unlimited members` },
{ text: t`Custom workspace username` },
{ text: t`Configurable user roles and permissions` },
{ text: t`Custom branding` },
], ],
highlighted: true, highlighted: true,
showPrice: true, showPrice: true,
showPriceSuffix: false, showPriceSuffix: true,
ctaSubtext: t`14 day free trial · Switch plans or cancel anytime`,
mostPopular: true,
}, },
{ {
name: t`Enterprise`, name: t`Self Host`,
id: "tier-enterprise", id: "tier-self-host",
href: "mailto:support@kan.bn?subject=Enterprise Inquiry", href: "https://github.com/kanbn/kan",
buttonText: t`Contact Sales`, buttonText: t`View docs`,
price: { monthly: t`Contact us`, annually: t`Contact us` }, price: { monthly: "-", annually: "-" },
description: t`Advanced security, compliance, and dedicated support for large organizations.`, description: t`Host Kan on your own infrastructure. Ideal for organisations that need complete control over their data.`,
bestFor: t`Best for large organizations with advanced needs`, featureHeader: t`Complete control and ownership:`,
featureHeader: undefined,
features: [ features: [
{ text: t`All Pro features +` }, t`Run on your own infrastructure`,
{ text: t`SAML SSO` }, t`Own your data`,
{ text: t`Advanced admin controls` }, t`Custom domain`,
{ text: t`Dedicated account manager` },
{ text: t`Custom SLA` },
{ text: t`On-premise deployment option` },
], ],
highlighted: false,
showPrice: true,
showPriceSuffix: false,
ctaSubtext: undefined,
mostPopular: false, mostPopular: false,
showPrice: false,
}, },
]; ];
return ( return (
<> <>
<div className="mx-auto max-w-7xl px-4"> <div className="flex flex-col items-center justify-center px-4 pb-10">
<div className="mb-8 flex items-center justify-center"> <div className="mt-14 flex flex-col items-center justify-center">
<div className="mb-8 flex items-center gap-2 rounded-full border bg-white px-4 py-1.5 text-center text-xs font-bold text-gray-800 dark:border-dark-300 dark:bg-dark-1000 dark:text-gray-800 lg:text-sm">
<HiBolt />
<p>{t`Launch offer: unlimited seats for just $29/month with Pro`}</p>
</div>
<fieldset aria-label={t`Payment frequency`}> <fieldset aria-label={t`Payment frequency`}>
<RadioGroup <RadioGroup
value={frequency} value={frequency}
onChange={(value) => setFrequency(value)} onChange={(value) => setFrequency(value)}
className="flex gap-1 rounded-lg border border-light-300 bg-light-50 p-1 dark:border-dark-300 dark:bg-dark-50" className="grid grid-cols-2 gap-x-1 rounded-full p-1 text-center text-xs/5 font-semibold ring-1 ring-inset ring-light-600 dark:ring-dark-600"
> >
{frequencies.map((option) => ( {frequencies.map((option) => (
<Radio <Radio
key={option.value} key={option.value}
value={option} value={option}
className={twMerge( className={twMerge(
"cursor-pointer rounded-md px-4 py-1.5 text-sm font-medium transition-colors", "cursor-pointer rounded-full px-2.5 py-1 text-xs transition-colors lg:text-sm",
frequency?.value === option.value frequency?.value === option.value
? "bg-white text-light-1000 shadow-sm dark:bg-dark-100 dark:text-dark-1000" ? "bg-dark-50 text-white dark:bg-light-50 dark:text-dark-50"
: "text-light-600 dark:text-dark-700", : "text-light-900 hover:bg-light-100 dark:hover:bg-dark-100",
)} )}
> >
{option.label} {option.label}
@@ -140,129 +106,103 @@ const Pricing = ({
</RadioGroup> </RadioGroup>
</fieldset> </fieldset>
</div> </div>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <div className="isolate mx-auto mb-10 grid max-w-md grid-cols-1 gap-8 px-4 lg:mx-0 lg:max-w-none lg:grid-cols-3">
{tiers.map((tier) => ( {tiers.map((tier) => (
<div <div
key={tier.id} key={tier.id}
className={twMerge( className={twMerge(
"relative flex h-full flex-col rounded-lg border p-6", tier.highlighted
tier.mostPopular ? "bg-dark-50 ring-1 ring-dark-50 dark:ring-dark-800"
? "border-light-400 bg-light-200 dark:border-dark-400 dark:bg-dark-100" : "bg-light-50 ring-1 ring-light-300 dark:bg-dark-50 dark:ring-dark-300",
: "border-light-300 bg-light-50 dark:border-dark-300 dark:bg-dark-50", "rounded-3xl p-8 xl:p-10",
)} )}
> >
{tier.id === "tier-pro" && ( <div className="flex items-center justify-between gap-x-4">
<div className="absolute right-2 top-2 z-10"> <h3
<span className="inline-flex items-center gap-1 rounded-full border border-light-400 bg-light-100 px-3 py-1 text-center text-xs text-dark-600 dark:border-dark-400 dark:bg-dark-100 dark:text-dark-900"> id={tier.id}
<HiBolt className="h-3 w-3 -ml-0.5" /> className={twMerge(
{t`Launch offer`} tier.highlighted
</span> ? "text-dark-1000 dark:text-dark-1000"
</div> : "text-dark-50 dark:text-dark-1000",
)} "text-lg/8 font-semibold",
<h3 )}
id={tier.id} >
{tier.name}
</h3>
{tier.highlighted && (
<p className="rounded-full bg-light-50 px-2.5 py-1 text-[12px] font-semibold text-dark-500 dark:bg-dark-1000 dark:text-dark-50">
{t`14 day free trial`}
</p>
)}
</div>
<p
className={twMerge( className={twMerge(
"mb-4 flex items-center text-base font-semibold", "mt-4 text-sm/6 text-dark-950 dark:text-dark-900",
tier.mostPopular tier.highlighted ? "text-light-100" : "text-dark-50",
? "text-light-1000 dark:text-dark-1000"
: "text-light-1000 dark:text-dark-1000",
)} )}
> >
{tier.name} {tier.description}
{tier.id === "tier-pro" && <span className="ml-1 text-xl leading-none"></span>} </p>
</h3> <p className="mt-6 flex items-baseline gap-x-1">
<div className="mb-4"> <span
<p className="flex items-baseline gap-x-1"> className={twMerge(
<span "text-3xl font-semibold tracking-tight text-light-100",
className={twMerge( tier.highlighted
"text-2xl font-semibold", ? "text-light-50 dark:text-dark-1000"
tier.mostPopular : "text-gray-900 dark:text-dark-1000",
? "text-light-1000 dark:text-dark-1000" !tier.showPrice && "opacity-0",
: "text-light-1000 dark:text-dark-1000", )}
!tier.showPrice && "opacity-0", >
)} {tier.price[frequency?.value ?? "monthly"]}
> </span>
{tier.price[frequency?.value ?? "monthly"]} {tier.showPriceSuffix && (
<span className="text-sm/6 font-semibold text-light-50 dark:text-dark-900">
{frequency?.priceSuffix}
</span> </span>
{tier.id === "tier-pro" && (
<span
className={twMerge(
"text-2xl font-semibold line-through",
tier.mostPopular
? "text-light-600 dark:text-dark-600"
: "text-light-600 dark:text-dark-600",
)}
>
{frequency?.value === "annually" ? "$79" : "$100"}
</span>
)}
{tier.showPriceSuffix && (
<span className="text-sm font-normal text-dark-400 dark:text-dark-700">
{frequency?.priceSuffix}
</span>
)}
{tier.id === "tier-pro" && (
<span className="text-sm font-normal text-dark-400 dark:text-dark-700">
{t`/month`}
</span>
)}
</p>
</div>
{tier.bestFor && (
<p className="mb-6 text-sm text-dark-400 dark:text-dark-700">
{tier.bestFor}
</p>
)}
<div className="mb-6">
{tier.id === "tier-enterprise" ? (
<a
href={tier.href}
aria-describedby={tier.id}
className="block w-full rounded-md border border-dark-200 bg-transparent px-4 py-2 text-center text-sm font-medium text-dark-50 transition-colors hover:bg-dark-50 hover:text-light-50 dark:border-dark-300 dark:text-dark-1000 dark:hover:bg-dark-200"
>
{tier.buttonText}
</a>
) : (
<>
<Link
href={tier.href}
aria-describedby={tier.id}
className={twMerge(
"block w-full rounded-md px-4 py-2 text-center text-sm font-medium transition-colors",
tier.mostPopular
? "bg-dark-50 text-light-50 shadow-sm hover:bg-dark-100 dark:bg-dark-1000 dark:text-dark-50 dark:hover:bg-dark-900"
: "border border-dark-200 bg-transparent text-dark-50 transition-colors hover:bg-dark-50 hover:text-light-50 dark:border-dark-300 dark:text-dark-1000 dark:hover:bg-dark-200",
)}
>
{tier.buttonText}
</Link>
</>
)} )}
</div> </p>
<ul role="list" className="space-y-2.5"> <Link
{tier.features.map((feature, index) => ( href={tier.href}
<li aria-describedby={tier.id}
key={`${tier.id}-feature-${index}`} className={twMerge(
className="flex items-start gap-x-2.5" tier.highlighted
> ? "bg-light-50 text-dark-50 shadow-sm dark:bg-dark-1000 dark:text-dark-50"
<HiCheckCircle : "bg-dark-50 text-light-50 dark:bg-dark-200 dark:text-dark-1000",
className="mt-0.5 h-4 w-4 shrink-0 text-light-1000 dark:text-dark-1000" "mt-6 block rounded-md px-3 py-2 text-center text-sm/6 font-semibold focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600",
/> )}
<span className="text-sm text-dark-600 dark:text-dark-800"> >
{feature.text} {tier.buttonText}
</span> </Link>
<p
className={twMerge(
"mt-8 text-sm/6 font-bold",
tier.highlighted
? "text-light-100 dark:text-dark-1000"
: "text-dark-50 dark:text-dark-1000",
)}
>
{tier.featureHeader}
</p>
<ul
role="list"
className={twMerge(
"mt-2 space-y-3 text-sm/6 text-light-600",
tier.highlighted
? "text-light-100 dark:text-dark-1000"
: "text-dark-50 dark:text-dark-1000",
)}
>
{tier.features.map((feature) => (
<li key={feature} className="flex items-center gap-x-3">
<HiCheckCircle className="h-5 w-5" />
{feature}
</li> </li>
))} ))}
</ul> </ul>
</div> </div>
))} ))}
</div>
<div className="mx-auto mt-8 max-w-3xl px-4 text-center">
<div className="inline-flex items-center gap-2 rounded-full border bg-light-50 px-4 py-1 text-center text-xs text-light-1000 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900">
<p>{t`14 day free trial · Switch plans or cancel anytime`}</p>
</div>
</div>
</div> </div>
</> </>
); );

View File

@@ -43,8 +43,8 @@ export default function PricingView() {
<p className="mt-4 text-center text-3xl font-bold text-light-1000 dark:text-dark-1000 lg:text-5xl"> <p className="mt-4 text-center text-3xl font-bold text-light-1000 dark:text-dark-1000 lg:text-5xl">
{t`Simple pricing`} {t`Simple pricing`}
</p> </p>
<p className="text:md lg:text-md mt-6 max-w-[300px] text-center text-light-950 dark:text-dark-900"> <p className="text:md lg:text-md mt-6 max-w-[500px] text-center text-light-950 dark:text-dark-900">
{t`Start free. Upgrade as your team grows. No hidden fees, no contracts.`} {t`Get started for free, with no usage limits. For collaboration, upgrade to a plan that fits the size of your team.`}
</p> </p>
</div> </div>
<PricingTiers <PricingTiers
@@ -59,9 +59,23 @@ export default function PricingView() {
</div> </div>
<div className="pb-22 flex flex-col items-center justify-center px-4"> <div className="pb-22 flex flex-col items-center justify-center px-4">
<div className="flex items-center gap-2 rounded-full border bg-light-50 px-4 py-1 text-center text-xs text-light-1000 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900 lg:text-sm">
<p>{t`Features`}</p>
</div>
<div className="mt-10 w-full"> <p className="mt-4 text-center text-3xl font-bold text-light-1000 dark:text-dark-1000 lg:text-4xl">
{t`Feature breakdown`}
</p>
<p className="text-md lg:text-md my-4 max-w-[500px] text-center text-light-950 dark:text-dark-900">
{t`Compare our features to see why Kan is the best choice.`}
</p>
<div className="mt-2">
<Button variant="primary" href="/signup">
{t`Get started`}
</Button>
</div>
<div className="mt-10 w-full max-w-[900px]">
<FeatureComparisonTable <FeatureComparisonTable
frequencyValue={frequency?.value ?? "annually"} frequencyValue={frequency?.value ?? "annually"}
/> />

View File

@@ -142,18 +142,9 @@ export const attachmentRouter = createTRPCRouter({
createdBy: userId, createdBy: userId,
}); });
if (!attachment) {
throw new TRPCError({
message: "Failed to create attachment",
code: "INTERNAL_SERVER_ERROR",
});
}
await cardActivityRepo.create(ctx.db, { await cardActivityRepo.create(ctx.db, {
type: "card.updated.attachment.added", type: "card.updated.attachment.added",
cardId: card.id, cardId: card.id,
attachmentId: attachment.id,
toTitle: input.originalFilename,
createdBy: userId, createdBy: userId,
}); });
@@ -215,8 +206,6 @@ export const attachmentRouter = createTRPCRouter({
await cardActivityRepo.create(ctx.db, { await cardActivityRepo.create(ctx.db, {
type: "card.updated.attachment.removed", type: "card.updated.attachment.removed",
cardId: attachment.cardId, cardId: attachment.cardId,
attachmentId: attachment.id,
fromTitle: attachment.originalFilename,
createdBy: userId, createdBy: userId,
}); });

View File

@@ -173,31 +173,8 @@ export const boardRouter = createTRPCRouter({
} }
: result.workspace; : result.workspace;
// Generate presigned URLs for card member avatars
const listsWithAvatarUrls = await Promise.all(
result.lists.map(async (list) => ({
...list,
cards: await Promise.all(
list.cards.map(async (card) => ({
...card,
members: await Promise.all(
card.members.map(async (member) => {
if (!member.user?.image) return member;
const avatarUrl = await generateAvatarUrl(member.user.image);
return {
...member,
user: { ...member.user, image: avatarUrl },
};
}),
),
})),
),
})),
);
return { return {
...result, ...result,
lists: listsWithAvatarUrls,
workspace: workspaceWithAvatarUrls, workspace: workspaceWithAvatarUrls,
}; };
}), }),

View File

@@ -11,16 +11,15 @@ import { createPlugins } from "./plugins";
import { configuredProviders } from "./providers"; import { configuredProviders } from "./providers";
export const initAuth = (db: dbClient) => { export const initAuth = (db: dbClient) => {
const baseURL = env("NEXT_PUBLIC_BASE_URL") || env("BETTER_AUTH_URL");
const trustedOrigins = env("BETTER_AUTH_TRUSTED_ORIGINS")?.split(",") ?? [];
return betterAuth({ return betterAuth({
secret: env("BETTER_AUTH_SECRET"), secret: env("BETTER_AUTH_SECRET"),
baseURL, baseURL: env("NEXT_PUBLIC_BASE_URL"),
trustedOrigins: [ trustedOrigins: env("BETTER_AUTH_TRUSTED_ORIGINS")
...(baseURL ? [baseURL] : []), ? [
...trustedOrigins, env("NEXT_PUBLIC_BASE_URL") ?? "",
], ...(env("BETTER_AUTH_TRUSTED_ORIGINS")?.split(",") ?? []),
]
: [env("NEXT_PUBLIC_BASE_URL") ?? ""],
database: drizzleAdapter(db, { database: drizzleAdapter(db, {
provider: "pg", provider: "pg",
schema: { schema: {

View File

@@ -5,7 +5,6 @@ import { magicLink } from "better-auth/plugins/magic-link";
import type { dbClient } from "@kan/db/client"; import type { dbClient } from "@kan/db/client";
import * as memberRepo from "@kan/db/repository/member.repo"; import * as memberRepo from "@kan/db/repository/member.repo";
import * as subscriptionRepo from "@kan/db/repository/subscription.repo"; import * as subscriptionRepo from "@kan/db/repository/subscription.repo";
import * as userRepo from "@kan/db/repository/user.repo";
import * as workspaceRepo from "@kan/db/repository/workspace.repo"; import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { generateUID } from "@kan/shared/utils"; import { generateUID } from "@kan/shared/utils";
import { sendEmail } from "@kan/email"; import { sendEmail } from "@kan/email";
@@ -134,10 +133,10 @@ export function createPlugins(db: dbClient) {
if (workspace?.id) { if (workspace?.id) {
await memberRepo.pauseAllMembers(db, workspace.id); await memberRepo.pauseAllMembers(db, workspace.id);
// Reset slug to publicId, or generate a UID if publicId is taken // Reset slug to publicId, or generate a UID if publicId is taken
let newSlug = workspace.publicId; let newSlug = workspace.publicId;
if (workspace.slug !== workspace.publicId) { if (workspace.slug !== workspace.publicId) {
const isPublicIdAvailable = await workspaceRepo.isWorkspaceSlugAvailable( const isPublicIdAvailable = await workspaceRepo.isWorkspaceSlugAvailable(
db, db,
@@ -147,7 +146,7 @@ export function createPlugins(db: dbClient) {
newSlug = generateUID(); newSlug = generateUID();
} }
} }
await workspaceRepo.update(db, subscription.referenceId, { await workspaceRepo.update(db, subscription.referenceId, {
plan: "free", plan: "free",
slug: newSlug, slug: newSlug,
@@ -172,69 +171,19 @@ export function createPlugins(db: dbClient) {
magicLink({ magicLink({
expiresIn: 60 * 60 * 24 * 7, // 7 days expiresIn: 60 * 60 * 24 * 7, // 7 days
sendMagicLink: async ({ email, url }) => { sendMagicLink: async ({ email, url }) => {
const decodedUrl = decodeURIComponent(url); if (url.includes("type=invite")) {
console.log("Sending magic link to:", email, "URL:", url);
console.log(
"Magic link contains invite:",
decodedUrl.includes("type=invite"),
);
if (decodedUrl.includes("type=invite")) {
let inviterName = "";
let workspaceName = "";
try {
const urlObj = new URL(url);
const callbackUrl = urlObj.searchParams.get("callbackURL");
if (callbackUrl) {
const callbackParams = new URL(
callbackUrl,
process.env.NEXT_PUBLIC_BASE_URL,
).searchParams;
const memberPublicId = callbackParams.get("memberPublicId");
if (memberPublicId) {
const member = await memberRepo.getByPublicId(
db,
memberPublicId,
);
if (member) {
const [workspace, inviter] = await Promise.all([
workspaceRepo.getById(db, member.workspaceId),
userRepo.getById(db, member.createdBy),
]);
if (workspace) workspaceName = workspace.name;
if (inviter) inviterName = inviter.name ?? "";
}
}
}
} catch (error) {
console.error("Failed to fetch invite details:", error);
}
await sendEmail( await sendEmail(
email, email,
workspaceName "Invitation to join workspace",
? `Invitation to join the workspace ${workspaceName}`
: "Invitation to join workspace",
"JOIN_WORKSPACE", "JOIN_WORKSPACE",
{ {
magicLoginUrl: url, magicLoginUrl: url,
inviterName,
workspaceName,
}, },
); );
} else { } else {
await sendEmail( await sendEmail(email, "Sign in to kan.bn", "MAGIC_LINK", {
email, magicLoginUrl: url,
process.env.NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY === "true" });
? "Sign in to your account"
: "Sign in to Kan",
"MAGIC_LINK",
{
magicLoginUrl: url,
},
);
} }
}, },
}), }),

View File

@@ -1,6 +0,0 @@
ALTER TABLE "card_activity" ADD COLUMN "attachmentId" bigint;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "card_activity" ADD CONSTRAINT "card_activity_attachmentId_card_attachment_id_fk" FOREIGN KEY ("attachmentId") REFERENCES "public"."card_attachment"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

File diff suppressed because it is too large Load Diff

View File

@@ -183,13 +183,6 @@
"when": 1770500457005, "when": 1770500457005,
"tag": "20260207214056_AddNotificationsTable", "tag": "20260207214056_AddNotificationsTable",
"breakpoints": true "breakpoints": true
},
{
"idx": 26,
"version": "7",
"when": 1770521594167,
"tag": "20260208033314_AddAttachmentToActivity",
"breakpoints": true
} }
] ]
} }

View File

@@ -33,7 +33,6 @@ export const create = async (
fromDueDate?: Date; fromDueDate?: Date;
toDueDate?: Date; toDueDate?: Date;
sourceBoardId?: number; sourceBoardId?: number;
attachmentId?: number;
}, },
) => { ) => {
const [result] = await db const [result] = await db
@@ -59,7 +58,6 @@ export const create = async (
fromDueDate: activityInput.fromDueDate, fromDueDate: activityInput.fromDueDate,
toDueDate: activityInput.toDueDate, toDueDate: activityInput.toDueDate,
sourceBoardId: activityInput.sourceBoardId, sourceBoardId: activityInput.sourceBoardId,
attachmentId: activityInput.attachmentId,
}) })
.returning({ id: cardActivities.id }); .returning({ id: cardActivities.id });
@@ -85,7 +83,6 @@ export const bulkCreate = async (
fromDueDate?: Date; fromDueDate?: Date;
toDueDate?: Date; toDueDate?: Date;
sourceBoardId?: number; sourceBoardId?: number;
attachmentId?: number;
}[], }[],
) => { ) => {
const activitiesWithPublicIds = activityInputs.map((activity) => ({ const activitiesWithPublicIds = activityInputs.map((activity) => ({
@@ -194,13 +191,6 @@ export const getPaginatedActivities = async (
deletedAt: true, deletedAt: true,
}, },
}, },
attachment: {
columns: {
publicId: true,
filename: true,
originalFilename: true,
},
},
}, },
orderBy: asc(cardActivities.createdAt), // required for merging and pagination orderBy: asc(cardActivities.createdAt), // required for merging and pagination
limit: limit + 1, // fetch one extra to check if there are more limit: limit + 1, // fetch one extra to check if there are more

View File

@@ -147,10 +147,6 @@ export const cardActivities = pgTable("card_activity", {
() => boards.id, () => boards.id,
{ onDelete: "set null" }, { onDelete: "set null" },
), ),
attachmentId: bigint("attachmentId", { mode: "number" }).references(
() => cardAttachments.id,
{ onDelete: "cascade" },
),
}).enableRLS(); }).enableRLS();
export const cardActivitiesRelations = relations(cardActivities, ({ one }) => ({ export const cardActivitiesRelations = relations(cardActivities, ({ one }) => ({
@@ -194,11 +190,6 @@ export const cardActivitiesRelations = relations(cardActivities, ({ one }) => ({
references: [comments.id], references: [comments.id],
relationName: "cardActivitiesComment", relationName: "cardActivitiesComment",
}), }),
attachment: one(cardAttachments, {
fields: [cardActivities.attachmentId],
references: [cardAttachments.id],
relationName: "cardActivitiesAttachment",
}),
})); }));
export const cardsToLabels = pgTable( export const cardsToLabels = pgTable(

View File

@@ -13,16 +13,12 @@ import * as React from "react";
export const JoinWorkspaceTemplate = ({ export const JoinWorkspaceTemplate = ({
magicLoginUrl, magicLoginUrl,
inviterName,
workspaceName,
}: { }: {
magicLoginUrl?: string; magicLoginUrl?: string;
inviterName?: string;
workspaceName?: string;
}) => ( }) => (
<Html> <Html>
<Head /> <Head />
<Preview>Join {workspaceName ?? "workspace"} on kan.bn</Preview> <Preview>Log in with this magic link</Preview>
<Body style={{ backgroundColor: "white" }}> <Body style={{ backgroundColor: "white" }}>
<Container <Container
style={{ style={{
@@ -42,16 +38,12 @@ export const JoinWorkspaceTemplate = ({
color: "#232323", color: "#232323",
}} }}
> >
{env("NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY") !== "true" && "Kan"} kan.bn
</Heading> </Heading>
<Heading <Heading
style={{ fontSize: "24px", fontWeight: "bold", color: "#232323" }} style={{ fontSize: "24px", fontWeight: "bold", color: "#232323" }}
> >
{inviterName Login to your Kan account
? `${inviterName} invited you to join ${
workspaceName ?? "a workspace"
}`
: `You've been invited to join ${workspaceName ?? "a workspace"}`}
</Heading> </Heading>
<Text <Text
style={{ style={{
@@ -60,7 +52,7 @@ export const JoinWorkspaceTemplate = ({
color: "#232323", color: "#232323",
}} }}
> >
Click the button below to add this workspace to your account. Click the button below to instantly login to your account.
</Text> </Text>
<Button <Button
target="_blank" target="_blank"
@@ -79,7 +71,7 @@ export const JoinWorkspaceTemplate = ({
color: "white", color: "white",
}} }}
> >
Join {workspaceName ?? "workspace"} Login to your account
</Button> </Button>
<Text <Text
style={{ style={{
@@ -88,29 +80,25 @@ export const JoinWorkspaceTemplate = ({
color: "#7e7e7e", color: "#7e7e7e",
}} }}
> >
If you don&apos;t want to join this workspace, you can safely ignore this email. If you didn&apos;t try to login, you can safely ignore this email.
</Text>
<Hr
style={{
marginTop: "2.5rem",
marginBottom: "2rem",
borderWidth: "1px",
}}
/>
<Text style={{ color: "#7e7e7e" }}>
<Link
href={env("NEXT_PUBLIC_BASE_URL")}
target="_blank"
style={{ color: "#7e7e7e", textDecoration: "underline" }}
>
Kan
</Link>
, the open source Trello alternative.
</Text> </Text>
{env("NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY") !== "true" && (
<>
<Hr
style={{
marginTop: "2.5rem",
marginBottom: "2rem",
borderWidth: "1px",
}}
/>
<Text style={{ color: "#7e7e7e" }}>
<Link
href={env("NEXT_PUBLIC_BASE_URL")}
target="_blank"
style={{ color: "#7e7e7e", textDecoration: "underline" }}
>
Kan
</Link>
, the open source Trello alternative.
</Text>
</>
)}
</Container> </Container>
</Body> </Body>
</Html> </Html>

301
pnpm-lock.yaml generated
View File

@@ -82,9 +82,6 @@ importers:
'@aws-sdk/client-s3': '@aws-sdk/client-s3':
specifier: ^3.802.0 specifier: ^3.802.0
version: 3.879.0 version: 3.879.0
'@aws-sdk/lib-storage':
specifier: ^3.802.0
version: 3.985.0(@aws-sdk/client-s3@3.879.0)
'@aws-sdk/s3-request-presigner': '@aws-sdk/s3-request-presigner':
specifier: ^3.812.0 specifier: ^3.812.0
version: 3.879.0 version: 3.879.0
@@ -740,12 +737,6 @@ packages:
resolution: {integrity: sha512-Jy4uPFfGzHk1Mxy+/Wr43vuw9yXsE2yiF4e4598vc3aJfO0YtA2nSfbKD3PNKRORwXbeKqWPfph9SCKQpWoxEg==} resolution: {integrity: sha512-Jy4uPFfGzHk1Mxy+/Wr43vuw9yXsE2yiF4e4598vc3aJfO0YtA2nSfbKD3PNKRORwXbeKqWPfph9SCKQpWoxEg==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@aws-sdk/lib-storage@3.985.0':
resolution: {integrity: sha512-EnqXf2E+5+7e5zuz8mPl+3Dk/DI0ObSL8s7Tsf6H8FL/p2BJw2LhgtxPnFZUmpanpoMO0hC9/qjXNd+aGPV0/w==}
engines: {node: '>=20.0.0'}
peerDependencies:
'@aws-sdk/client-s3': ^3.985.0
'@aws-sdk/middleware-bucket-endpoint@3.873.0': '@aws-sdk/middleware-bucket-endpoint@3.873.0':
resolution: {integrity: sha512-b4bvr0QdADeTUs+lPc9Z48kXzbKHXQKgTvxx/jXDgSW9tv4KmYPO1gIj6Z9dcrBkRWQuUtSW3Tu2S5n6pe+zeg==} resolution: {integrity: sha512-b4bvr0QdADeTUs+lPc9Z48kXzbKHXQKgTvxx/jXDgSW9tv4KmYPO1gIj6Z9dcrBkRWQuUtSW3Tu2S5n6pe+zeg==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@@ -3312,10 +3303,6 @@ packages:
resolution: {integrity: sha512-jcrqdTQurIrBbUm4W2YdLVMQDoL0sA9DTxYd2s+R/y+2U9NLOP7Xf/YqfSg1FZhlZIYEnvk2mwbyvIfdLEPo8g==} resolution: {integrity: sha512-jcrqdTQurIrBbUm4W2YdLVMQDoL0sA9DTxYd2s+R/y+2U9NLOP7Xf/YqfSg1FZhlZIYEnvk2mwbyvIfdLEPo8g==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/abort-controller@4.2.8':
resolution: {integrity: sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==}
engines: {node: '>=18.0.0'}
'@smithy/chunked-blob-reader-native@4.0.0': '@smithy/chunked-blob-reader-native@4.0.0':
resolution: {integrity: sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig==} resolution: {integrity: sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@@ -3328,10 +3315,6 @@ packages:
resolution: {integrity: sha512-viuHMxBAqydkB0AfWwHIdwf/PRH2z5KHGUzqyRtS/Wv+n3IHI993Sk76VCA7dD/+GzgGOmlJDITfPcJC1nIVIw==} resolution: {integrity: sha512-viuHMxBAqydkB0AfWwHIdwf/PRH2z5KHGUzqyRtS/Wv+n3IHI993Sk76VCA7dD/+GzgGOmlJDITfPcJC1nIVIw==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/core@3.22.1':
resolution: {integrity: sha512-x3ie6Crr58MWrm4viHqqy2Du2rHYZjwu8BekasrQx4ca+Y24dzVAwq3yErdqIbc2G3I0kLQA13PQ+/rde+u65g==}
engines: {node: '>=18.0.0'}
'@smithy/core@3.9.0': '@smithy/core@3.9.0':
resolution: {integrity: sha512-B/GknvCfS3llXd/b++hcrwIuqnEozQDnRL4sBmOac5/z/dr0/yG1PURNPOyU4Lsiy1IyTj8scPxVqRs5dYWf6A==} resolution: {integrity: sha512-B/GknvCfS3llXd/b++hcrwIuqnEozQDnRL4sBmOac5/z/dr0/yG1PURNPOyU4Lsiy1IyTj8scPxVqRs5dYWf6A==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@@ -3364,10 +3347,6 @@ packages:
resolution: {integrity: sha512-61WjM0PWmZJR+SnmzaKI7t7G0UkkNFboDpzIdzSoy7TByUzlxo18Qlh9s71qug4AY4hlH/CwXdubMtkcNEb/sQ==} resolution: {integrity: sha512-61WjM0PWmZJR+SnmzaKI7t7G0UkkNFboDpzIdzSoy7TByUzlxo18Qlh9s71qug4AY4hlH/CwXdubMtkcNEb/sQ==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/fetch-http-handler@5.3.9':
resolution: {integrity: sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==}
engines: {node: '>=18.0.0'}
'@smithy/hash-blob-browser@4.0.5': '@smithy/hash-blob-browser@4.0.5':
resolution: {integrity: sha512-F7MmCd3FH/Q2edhcKd+qulWkwfChHbc9nhguBlVjSUE6hVHhec3q6uPQ+0u69S6ppvLtR3eStfCuEKMXBXhvvA==} resolution: {integrity: sha512-F7MmCd3FH/Q2edhcKd+qulWkwfChHbc9nhguBlVjSUE6hVHhec3q6uPQ+0u69S6ppvLtR3eStfCuEKMXBXhvvA==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@@ -3392,10 +3371,6 @@ packages:
resolution: {integrity: sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==} resolution: {integrity: sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/is-array-buffer@4.2.0':
resolution: {integrity: sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==}
engines: {node: '>=18.0.0'}
'@smithy/md5-js@4.0.5': '@smithy/md5-js@4.0.5':
resolution: {integrity: sha512-8n2XCwdUbGr8W/XhMTaxILkVlw2QebkVTn5tm3HOcbPbOpWg89zr6dPXsH8xbeTsbTXlJvlJNTQsKAIoqQGbdA==} resolution: {integrity: sha512-8n2XCwdUbGr8W/XhMTaxILkVlw2QebkVTn5tm3HOcbPbOpWg89zr6dPXsH8xbeTsbTXlJvlJNTQsKAIoqQGbdA==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@@ -3408,10 +3383,6 @@ packages:
resolution: {integrity: sha512-EAlEPncqo03siNZJ9Tm6adKCQ+sw5fNU8ncxWwaH0zTCwMPsgmERTi6CEKaermZdgJb+4Yvh0NFm36HeO4PGgQ==} resolution: {integrity: sha512-EAlEPncqo03siNZJ9Tm6adKCQ+sw5fNU8ncxWwaH0zTCwMPsgmERTi6CEKaermZdgJb+4Yvh0NFm36HeO4PGgQ==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/middleware-endpoint@4.4.13':
resolution: {integrity: sha512-x6vn0PjYmGdNuKh/juUJJewZh7MoQ46jYaJ2mvekF4EesMuFfrl4LaW/k97Zjf8PTCPQmPgMvwewg7eNoH9n5w==}
engines: {node: '>=18.0.0'}
'@smithy/middleware-retry@4.1.20': '@smithy/middleware-retry@4.1.20':
resolution: {integrity: sha512-T3maNEm3Masae99eFdx1Q7PIqBBEVOvRd5hralqKZNeIivnoGNx5OFtI3DiZ5gCjUkl0mNondlzSXeVxkinh7Q==} resolution: {integrity: sha512-T3maNEm3Masae99eFdx1Q7PIqBBEVOvRd5hralqKZNeIivnoGNx5OFtI3DiZ5gCjUkl0mNondlzSXeVxkinh7Q==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@@ -3420,66 +3391,34 @@ packages:
resolution: {integrity: sha512-uAFFR4dpeoJPGz8x9mhxp+RPjo5wW0QEEIPPPbLXiRRWeCATf/Km3gKIVR5vaP8bN1kgsPhcEeh+IZvUlBv6Xg==} resolution: {integrity: sha512-uAFFR4dpeoJPGz8x9mhxp+RPjo5wW0QEEIPPPbLXiRRWeCATf/Km3gKIVR5vaP8bN1kgsPhcEeh+IZvUlBv6Xg==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/middleware-serde@4.2.9':
resolution: {integrity: sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==}
engines: {node: '>=18.0.0'}
'@smithy/middleware-stack@4.0.5': '@smithy/middleware-stack@4.0.5':
resolution: {integrity: sha512-/yoHDXZPh3ocRVyeWQFvC44u8seu3eYzZRveCMfgMOBcNKnAmOvjbL9+Cp5XKSIi9iYA9PECUuW2teDAk8T+OQ==} resolution: {integrity: sha512-/yoHDXZPh3ocRVyeWQFvC44u8seu3eYzZRveCMfgMOBcNKnAmOvjbL9+Cp5XKSIi9iYA9PECUuW2teDAk8T+OQ==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/middleware-stack@4.2.8':
resolution: {integrity: sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==}
engines: {node: '>=18.0.0'}
'@smithy/node-config-provider@4.1.4': '@smithy/node-config-provider@4.1.4':
resolution: {integrity: sha512-+UDQV/k42jLEPPHSn39l0Bmc4sB1xtdI9Gd47fzo/0PbXzJ7ylgaOByVjF5EeQIumkepnrJyfx86dPa9p47Y+w==} resolution: {integrity: sha512-+UDQV/k42jLEPPHSn39l0Bmc4sB1xtdI9Gd47fzo/0PbXzJ7ylgaOByVjF5EeQIumkepnrJyfx86dPa9p47Y+w==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/node-config-provider@4.3.8':
resolution: {integrity: sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==}
engines: {node: '>=18.0.0'}
'@smithy/node-http-handler@4.1.1': '@smithy/node-http-handler@4.1.1':
resolution: {integrity: sha512-RHnlHqFpoVdjSPPiYy/t40Zovf3BBHc2oemgD7VsVTFFZrU5erFFe0n52OANZZ/5sbshgD93sOh5r6I35Xmpaw==} resolution: {integrity: sha512-RHnlHqFpoVdjSPPiYy/t40Zovf3BBHc2oemgD7VsVTFFZrU5erFFe0n52OANZZ/5sbshgD93sOh5r6I35Xmpaw==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/node-http-handler@4.4.9':
resolution: {integrity: sha512-KX5Wml5mF+luxm1szW4QDz32e3NObgJ4Fyw+irhph4I/2geXwUy4jkIMUs5ZPGflRBeR6BUkC2wqIab4Llgm3w==}
engines: {node: '>=18.0.0'}
'@smithy/property-provider@4.0.5': '@smithy/property-provider@4.0.5':
resolution: {integrity: sha512-R/bswf59T/n9ZgfgUICAZoWYKBHcsVDurAGX88zsiUtOTA/xUAPyiT+qkNCPwFn43pZqN84M4MiUsbSGQmgFIQ==} resolution: {integrity: sha512-R/bswf59T/n9ZgfgUICAZoWYKBHcsVDurAGX88zsiUtOTA/xUAPyiT+qkNCPwFn43pZqN84M4MiUsbSGQmgFIQ==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/property-provider@4.2.8':
resolution: {integrity: sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==}
engines: {node: '>=18.0.0'}
'@smithy/protocol-http@5.1.3': '@smithy/protocol-http@5.1.3':
resolution: {integrity: sha512-fCJd2ZR7D22XhDY0l+92pUag/7je2BztPRQ01gU5bMChcyI0rlly7QFibnYHzcxDvccMjlpM/Q1ev8ceRIb48w==} resolution: {integrity: sha512-fCJd2ZR7D22XhDY0l+92pUag/7je2BztPRQ01gU5bMChcyI0rlly7QFibnYHzcxDvccMjlpM/Q1ev8ceRIb48w==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/protocol-http@5.3.8':
resolution: {integrity: sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==}
engines: {node: '>=18.0.0'}
'@smithy/querystring-builder@4.0.5': '@smithy/querystring-builder@4.0.5':
resolution: {integrity: sha512-NJeSCU57piZ56c+/wY+AbAw6rxCCAOZLCIniRE7wqvndqxcKKDOXzwWjrY7wGKEISfhL9gBbAaWWgHsUGedk+A==} resolution: {integrity: sha512-NJeSCU57piZ56c+/wY+AbAw6rxCCAOZLCIniRE7wqvndqxcKKDOXzwWjrY7wGKEISfhL9gBbAaWWgHsUGedk+A==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/querystring-builder@4.2.8':
resolution: {integrity: sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==}
engines: {node: '>=18.0.0'}
'@smithy/querystring-parser@4.0.5': '@smithy/querystring-parser@4.0.5':
resolution: {integrity: sha512-6SV7md2CzNG/WUeTjVe6Dj8noH32r4MnUeFKZrnVYsQxpGSIcphAanQMayi8jJLZAWm6pdM9ZXvKCpWOsIGg0w==} resolution: {integrity: sha512-6SV7md2CzNG/WUeTjVe6Dj8noH32r4MnUeFKZrnVYsQxpGSIcphAanQMayi8jJLZAWm6pdM9ZXvKCpWOsIGg0w==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/querystring-parser@4.2.8':
resolution: {integrity: sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==}
engines: {node: '>=18.0.0'}
'@smithy/service-error-classification@4.0.7': '@smithy/service-error-classification@4.0.7':
resolution: {integrity: sha512-XvRHOipqpwNhEjDf2L5gJowZEm5nsxC16pAZOeEcsygdjv9A2jdOh3YoDQvOXBGTsaJk6mNWtzWalOB9976Wlg==} resolution: {integrity: sha512-XvRHOipqpwNhEjDf2L5gJowZEm5nsxC16pAZOeEcsygdjv9A2jdOh3YoDQvOXBGTsaJk6mNWtzWalOB9976Wlg==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@@ -3488,26 +3427,14 @@ packages:
resolution: {integrity: sha512-YVVwehRDuehgoXdEL4r1tAAzdaDgaC9EQvhK0lEbfnbrd0bd5+CTQumbdPryX3J2shT7ZqQE+jPW4lmNBAB8JQ==} resolution: {integrity: sha512-YVVwehRDuehgoXdEL4r1tAAzdaDgaC9EQvhK0lEbfnbrd0bd5+CTQumbdPryX3J2shT7ZqQE+jPW4lmNBAB8JQ==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/shared-ini-file-loader@4.4.3':
resolution: {integrity: sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==}
engines: {node: '>=18.0.0'}
'@smithy/signature-v4@5.1.3': '@smithy/signature-v4@5.1.3':
resolution: {integrity: sha512-mARDSXSEgllNzMw6N+mC+r1AQlEBO3meEAkR/UlfAgnMzJUB3goRBWgip1EAMG99wh36MDqzo86SfIX5Y+VEaw==} resolution: {integrity: sha512-mARDSXSEgllNzMw6N+mC+r1AQlEBO3meEAkR/UlfAgnMzJUB3goRBWgip1EAMG99wh36MDqzo86SfIX5Y+VEaw==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/smithy-client@4.11.2':
resolution: {integrity: sha512-SCkGmFak/xC1n7hKRsUr6wOnBTJ3L22Qd4e8H1fQIuKTAjntwgU8lrdMe7uHdiT2mJAOWA/60qaW9tiMu69n1A==}
engines: {node: '>=18.0.0'}
'@smithy/smithy-client@4.5.0': '@smithy/smithy-client@4.5.0':
resolution: {integrity: sha512-ZSdE3vl0MuVbEwJBxSftm0J5nL/gw76xp5WF13zW9cN18MFuFXD5/LV0QD8P+sCU5bSWGyy6CTgUupE1HhOo1A==} resolution: {integrity: sha512-ZSdE3vl0MuVbEwJBxSftm0J5nL/gw76xp5WF13zW9cN18MFuFXD5/LV0QD8P+sCU5bSWGyy6CTgUupE1HhOo1A==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/types@4.12.0':
resolution: {integrity: sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==}
engines: {node: '>=18.0.0'}
'@smithy/types@4.3.2': '@smithy/types@4.3.2':
resolution: {integrity: sha512-QO4zghLxiQ5W9UZmX2Lo0nta2PuE1sSrXUYDoaB6HMR762C0P7v/HEPHf6ZdglTVssJG1bsrSBxdc3quvDSihw==} resolution: {integrity: sha512-QO4zghLxiQ5W9UZmX2Lo0nta2PuE1sSrXUYDoaB6HMR762C0P7v/HEPHf6ZdglTVssJG1bsrSBxdc3quvDSihw==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@@ -3516,26 +3443,14 @@ packages:
resolution: {integrity: sha512-j+733Um7f1/DXjYhCbvNXABV53NyCRRA54C7bNEIxNPs0YjfRxeMKjjgm2jvTYrciZyCjsicHwQ6Q0ylo+NAUw==} resolution: {integrity: sha512-j+733Um7f1/DXjYhCbvNXABV53NyCRRA54C7bNEIxNPs0YjfRxeMKjjgm2jvTYrciZyCjsicHwQ6Q0ylo+NAUw==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/url-parser@4.2.8':
resolution: {integrity: sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==}
engines: {node: '>=18.0.0'}
'@smithy/util-base64@4.0.0': '@smithy/util-base64@4.0.0':
resolution: {integrity: sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==} resolution: {integrity: sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/util-base64@4.3.0':
resolution: {integrity: sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==}
engines: {node: '>=18.0.0'}
'@smithy/util-body-length-browser@4.0.0': '@smithy/util-body-length-browser@4.0.0':
resolution: {integrity: sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==} resolution: {integrity: sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/util-body-length-browser@4.2.0':
resolution: {integrity: sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==}
engines: {node: '>=18.0.0'}
'@smithy/util-body-length-node@4.0.0': '@smithy/util-body-length-node@4.0.0':
resolution: {integrity: sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==} resolution: {integrity: sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@@ -3548,10 +3463,6 @@ packages:
resolution: {integrity: sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==} resolution: {integrity: sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/util-buffer-from@4.2.0':
resolution: {integrity: sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==}
engines: {node: '>=18.0.0'}
'@smithy/util-config-provider@4.0.0': '@smithy/util-config-provider@4.0.0':
resolution: {integrity: sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==} resolution: {integrity: sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@@ -3572,18 +3483,10 @@ packages:
resolution: {integrity: sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==} resolution: {integrity: sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/util-hex-encoding@4.2.0':
resolution: {integrity: sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==}
engines: {node: '>=18.0.0'}
'@smithy/util-middleware@4.0.5': '@smithy/util-middleware@4.0.5':
resolution: {integrity: sha512-N40PfqsZHRSsByGB81HhSo+uvMxEHT+9e255S53pfBw/wI6WKDI7Jw9oyu5tJTLwZzV5DsMha3ji8jk9dsHmQQ==} resolution: {integrity: sha512-N40PfqsZHRSsByGB81HhSo+uvMxEHT+9e255S53pfBw/wI6WKDI7Jw9oyu5tJTLwZzV5DsMha3ji8jk9dsHmQQ==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/util-middleware@4.2.8':
resolution: {integrity: sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==}
engines: {node: '>=18.0.0'}
'@smithy/util-retry@4.0.7': '@smithy/util-retry@4.0.7':
resolution: {integrity: sha512-TTO6rt0ppK70alZpkjwy+3nQlTiqNfoXja+qwuAchIEAIoSZW8Qyd76dvBv3I5bCpE38APafG23Y/u270NspiQ==} resolution: {integrity: sha512-TTO6rt0ppK70alZpkjwy+3nQlTiqNfoXja+qwuAchIEAIoSZW8Qyd76dvBv3I5bCpE38APafG23Y/u270NspiQ==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@@ -3592,18 +3495,10 @@ packages:
resolution: {integrity: sha512-vSKnvNZX2BXzl0U2RgCLOwWaAP9x/ddd/XobPK02pCbzRm5s55M53uwb1rl/Ts7RXZvdJZerPkA+en2FDghLuQ==} resolution: {integrity: sha512-vSKnvNZX2BXzl0U2RgCLOwWaAP9x/ddd/XobPK02pCbzRm5s55M53uwb1rl/Ts7RXZvdJZerPkA+en2FDghLuQ==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/util-stream@4.5.11':
resolution: {integrity: sha512-lKmZ0S/3Qj2OF5H1+VzvDLb6kRxGzZHq6f3rAsoSu5cTLGsn3v3VQBA8czkNNXlLjoFEtVu3OQT2jEeOtOE2CA==}
engines: {node: '>=18.0.0'}
'@smithy/util-uri-escape@4.0.0': '@smithy/util-uri-escape@4.0.0':
resolution: {integrity: sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==} resolution: {integrity: sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/util-uri-escape@4.2.0':
resolution: {integrity: sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==}
engines: {node: '>=18.0.0'}
'@smithy/util-utf8@2.3.0': '@smithy/util-utf8@2.3.0':
resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@@ -3612,18 +3507,10 @@ packages:
resolution: {integrity: sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==} resolution: {integrity: sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/util-utf8@4.2.0':
resolution: {integrity: sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==}
engines: {node: '>=18.0.0'}
'@smithy/util-waiter@4.0.7': '@smithy/util-waiter@4.0.7':
resolution: {integrity: sha512-mYqtQXPmrwvUljaHyGxYUIIRI3qjBTEb/f5QFi3A6VlxhpmZd5mWXn9W+qUkf2pVE1Hv3SqxefiZOPGdxmO64A==} resolution: {integrity: sha512-mYqtQXPmrwvUljaHyGxYUIIRI3qjBTEb/f5QFi3A6VlxhpmZd5mWXn9W+qUkf2pVE1Hv3SqxefiZOPGdxmO64A==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@smithy/uuid@1.1.0':
resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==}
engines: {node: '>=18.0.0'}
'@socket.io/component-emitter@3.1.2': '@socket.io/component-emitter@3.1.2':
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
@@ -4599,9 +4486,6 @@ packages:
buffer-from@1.1.2: buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
buffer@5.6.0:
resolution: {integrity: sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==}
buffer@5.7.1: buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
@@ -7876,9 +7760,6 @@ packages:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
stream-browserify@3.0.0:
resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==}
string-width@4.2.3: string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -9021,17 +8902,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- aws-crt - aws-crt
'@aws-sdk/lib-storage@3.985.0(@aws-sdk/client-s3@3.879.0)':
dependencies:
'@aws-sdk/client-s3': 3.879.0
'@smithy/abort-controller': 4.2.8
'@smithy/middleware-endpoint': 4.4.13
'@smithy/smithy-client': 4.11.2
buffer: 5.6.0
events: 3.3.0
stream-browserify: 3.0.0
tslib: 2.8.1
'@aws-sdk/middleware-bucket-endpoint@3.873.0': '@aws-sdk/middleware-bucket-endpoint@3.873.0':
dependencies: dependencies:
'@aws-sdk/types': 3.862.0 '@aws-sdk/types': 3.862.0
@@ -11466,11 +11336,6 @@ snapshots:
'@smithy/types': 4.3.2 '@smithy/types': 4.3.2
tslib: 2.8.1 tslib: 2.8.1
'@smithy/abort-controller@4.2.8':
dependencies:
'@smithy/types': 4.12.0
tslib: 2.8.1
'@smithy/chunked-blob-reader-native@4.0.0': '@smithy/chunked-blob-reader-native@4.0.0':
dependencies: dependencies:
'@smithy/util-base64': 4.0.0 '@smithy/util-base64': 4.0.0
@@ -11488,19 +11353,6 @@ snapshots:
'@smithy/util-middleware': 4.0.5 '@smithy/util-middleware': 4.0.5
tslib: 2.8.1 tslib: 2.8.1
'@smithy/core@3.22.1':
dependencies:
'@smithy/middleware-serde': 4.2.9
'@smithy/protocol-http': 5.3.8
'@smithy/types': 4.12.0
'@smithy/util-base64': 4.3.0
'@smithy/util-body-length-browser': 4.2.0
'@smithy/util-middleware': 4.2.8
'@smithy/util-stream': 4.5.11
'@smithy/util-utf8': 4.2.0
'@smithy/uuid': 1.1.0
tslib: 2.8.1
'@smithy/core@3.9.0': '@smithy/core@3.9.0':
dependencies: dependencies:
'@smithy/middleware-serde': 4.0.9 '@smithy/middleware-serde': 4.0.9
@@ -11561,14 +11413,6 @@ snapshots:
'@smithy/util-base64': 4.0.0 '@smithy/util-base64': 4.0.0
tslib: 2.8.1 tslib: 2.8.1
'@smithy/fetch-http-handler@5.3.9':
dependencies:
'@smithy/protocol-http': 5.3.8
'@smithy/querystring-builder': 4.2.8
'@smithy/types': 4.12.0
'@smithy/util-base64': 4.3.0
tslib: 2.8.1
'@smithy/hash-blob-browser@4.0.5': '@smithy/hash-blob-browser@4.0.5':
dependencies: dependencies:
'@smithy/chunked-blob-reader': 5.0.0 '@smithy/chunked-blob-reader': 5.0.0
@@ -11602,10 +11446,6 @@ snapshots:
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
'@smithy/is-array-buffer@4.2.0':
dependencies:
tslib: 2.8.1
'@smithy/md5-js@4.0.5': '@smithy/md5-js@4.0.5':
dependencies: dependencies:
'@smithy/types': 4.3.2 '@smithy/types': 4.3.2
@@ -11629,17 +11469,6 @@ snapshots:
'@smithy/util-middleware': 4.0.5 '@smithy/util-middleware': 4.0.5
tslib: 2.8.1 tslib: 2.8.1
'@smithy/middleware-endpoint@4.4.13':
dependencies:
'@smithy/core': 3.22.1
'@smithy/middleware-serde': 4.2.9
'@smithy/node-config-provider': 4.3.8
'@smithy/shared-ini-file-loader': 4.4.3
'@smithy/types': 4.12.0
'@smithy/url-parser': 4.2.8
'@smithy/util-middleware': 4.2.8
tslib: 2.8.1
'@smithy/middleware-retry@4.1.20': '@smithy/middleware-retry@4.1.20':
dependencies: dependencies:
'@smithy/node-config-provider': 4.1.4 '@smithy/node-config-provider': 4.1.4
@@ -11659,22 +11488,11 @@ snapshots:
'@smithy/types': 4.3.2 '@smithy/types': 4.3.2
tslib: 2.8.1 tslib: 2.8.1
'@smithy/middleware-serde@4.2.9':
dependencies:
'@smithy/protocol-http': 5.3.8
'@smithy/types': 4.12.0
tslib: 2.8.1
'@smithy/middleware-stack@4.0.5': '@smithy/middleware-stack@4.0.5':
dependencies: dependencies:
'@smithy/types': 4.3.2 '@smithy/types': 4.3.2
tslib: 2.8.1 tslib: 2.8.1
'@smithy/middleware-stack@4.2.8':
dependencies:
'@smithy/types': 4.12.0
tslib: 2.8.1
'@smithy/node-config-provider@4.1.4': '@smithy/node-config-provider@4.1.4':
dependencies: dependencies:
'@smithy/property-provider': 4.0.5 '@smithy/property-provider': 4.0.5
@@ -11682,13 +11500,6 @@ snapshots:
'@smithy/types': 4.3.2 '@smithy/types': 4.3.2
tslib: 2.8.1 tslib: 2.8.1
'@smithy/node-config-provider@4.3.8':
dependencies:
'@smithy/property-provider': 4.2.8
'@smithy/shared-ini-file-loader': 4.4.3
'@smithy/types': 4.12.0
tslib: 2.8.1
'@smithy/node-http-handler@4.1.1': '@smithy/node-http-handler@4.1.1':
dependencies: dependencies:
'@smithy/abort-controller': 4.0.5 '@smithy/abort-controller': 4.0.5
@@ -11697,56 +11508,27 @@ snapshots:
'@smithy/types': 4.3.2 '@smithy/types': 4.3.2
tslib: 2.8.1 tslib: 2.8.1
'@smithy/node-http-handler@4.4.9':
dependencies:
'@smithy/abort-controller': 4.2.8
'@smithy/protocol-http': 5.3.8
'@smithy/querystring-builder': 4.2.8
'@smithy/types': 4.12.0
tslib: 2.8.1
'@smithy/property-provider@4.0.5': '@smithy/property-provider@4.0.5':
dependencies: dependencies:
'@smithy/types': 4.3.2 '@smithy/types': 4.3.2
tslib: 2.8.1 tslib: 2.8.1
'@smithy/property-provider@4.2.8':
dependencies:
'@smithy/types': 4.12.0
tslib: 2.8.1
'@smithy/protocol-http@5.1.3': '@smithy/protocol-http@5.1.3':
dependencies: dependencies:
'@smithy/types': 4.3.2 '@smithy/types': 4.3.2
tslib: 2.8.1 tslib: 2.8.1
'@smithy/protocol-http@5.3.8':
dependencies:
'@smithy/types': 4.12.0
tslib: 2.8.1
'@smithy/querystring-builder@4.0.5': '@smithy/querystring-builder@4.0.5':
dependencies: dependencies:
'@smithy/types': 4.3.2 '@smithy/types': 4.3.2
'@smithy/util-uri-escape': 4.0.0 '@smithy/util-uri-escape': 4.0.0
tslib: 2.8.1 tslib: 2.8.1
'@smithy/querystring-builder@4.2.8':
dependencies:
'@smithy/types': 4.12.0
'@smithy/util-uri-escape': 4.2.0
tslib: 2.8.1
'@smithy/querystring-parser@4.0.5': '@smithy/querystring-parser@4.0.5':
dependencies: dependencies:
'@smithy/types': 4.3.2 '@smithy/types': 4.3.2
tslib: 2.8.1 tslib: 2.8.1
'@smithy/querystring-parser@4.2.8':
dependencies:
'@smithy/types': 4.12.0
tslib: 2.8.1
'@smithy/service-error-classification@4.0.7': '@smithy/service-error-classification@4.0.7':
dependencies: dependencies:
'@smithy/types': 4.3.2 '@smithy/types': 4.3.2
@@ -11756,11 +11538,6 @@ snapshots:
'@smithy/types': 4.3.2 '@smithy/types': 4.3.2
tslib: 2.8.1 tslib: 2.8.1
'@smithy/shared-ini-file-loader@4.4.3':
dependencies:
'@smithy/types': 4.12.0
tslib: 2.8.1
'@smithy/signature-v4@5.1.3': '@smithy/signature-v4@5.1.3':
dependencies: dependencies:
'@smithy/is-array-buffer': 4.0.0 '@smithy/is-array-buffer': 4.0.0
@@ -11772,16 +11549,6 @@ snapshots:
'@smithy/util-utf8': 4.0.0 '@smithy/util-utf8': 4.0.0
tslib: 2.8.1 tslib: 2.8.1
'@smithy/smithy-client@4.11.2':
dependencies:
'@smithy/core': 3.22.1
'@smithy/middleware-endpoint': 4.4.13
'@smithy/middleware-stack': 4.2.8
'@smithy/protocol-http': 5.3.8
'@smithy/types': 4.12.0
'@smithy/util-stream': 4.5.11
tslib: 2.8.1
'@smithy/smithy-client@4.5.0': '@smithy/smithy-client@4.5.0':
dependencies: dependencies:
'@smithy/core': 3.9.0 '@smithy/core': 3.9.0
@@ -11792,10 +11559,6 @@ snapshots:
'@smithy/util-stream': 4.2.4 '@smithy/util-stream': 4.2.4
tslib: 2.8.1 tslib: 2.8.1
'@smithy/types@4.12.0':
dependencies:
tslib: 2.8.1
'@smithy/types@4.3.2': '@smithy/types@4.3.2':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@@ -11806,32 +11569,16 @@ snapshots:
'@smithy/types': 4.3.2 '@smithy/types': 4.3.2
tslib: 2.8.1 tslib: 2.8.1
'@smithy/url-parser@4.2.8':
dependencies:
'@smithy/querystring-parser': 4.2.8
'@smithy/types': 4.12.0
tslib: 2.8.1
'@smithy/util-base64@4.0.0': '@smithy/util-base64@4.0.0':
dependencies: dependencies:
'@smithy/util-buffer-from': 4.0.0 '@smithy/util-buffer-from': 4.0.0
'@smithy/util-utf8': 4.0.0 '@smithy/util-utf8': 4.0.0
tslib: 2.8.1 tslib: 2.8.1
'@smithy/util-base64@4.3.0':
dependencies:
'@smithy/util-buffer-from': 4.2.0
'@smithy/util-utf8': 4.2.0
tslib: 2.8.1
'@smithy/util-body-length-browser@4.0.0': '@smithy/util-body-length-browser@4.0.0':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
'@smithy/util-body-length-browser@4.2.0':
dependencies:
tslib: 2.8.1
'@smithy/util-body-length-node@4.0.0': '@smithy/util-body-length-node@4.0.0':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@@ -11846,11 +11593,6 @@ snapshots:
'@smithy/is-array-buffer': 4.0.0 '@smithy/is-array-buffer': 4.0.0
tslib: 2.8.1 tslib: 2.8.1
'@smithy/util-buffer-from@4.2.0':
dependencies:
'@smithy/is-array-buffer': 4.2.0
tslib: 2.8.1
'@smithy/util-config-provider@4.0.0': '@smithy/util-config-provider@4.0.0':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@@ -11883,20 +11625,11 @@ snapshots:
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
'@smithy/util-hex-encoding@4.2.0':
dependencies:
tslib: 2.8.1
'@smithy/util-middleware@4.0.5': '@smithy/util-middleware@4.0.5':
dependencies: dependencies:
'@smithy/types': 4.3.2 '@smithy/types': 4.3.2
tslib: 2.8.1 tslib: 2.8.1
'@smithy/util-middleware@4.2.8':
dependencies:
'@smithy/types': 4.12.0
tslib: 2.8.1
'@smithy/util-retry@4.0.7': '@smithy/util-retry@4.0.7':
dependencies: dependencies:
'@smithy/service-error-classification': 4.0.7 '@smithy/service-error-classification': 4.0.7
@@ -11914,25 +11647,10 @@ snapshots:
'@smithy/util-utf8': 4.0.0 '@smithy/util-utf8': 4.0.0
tslib: 2.8.1 tslib: 2.8.1
'@smithy/util-stream@4.5.11':
dependencies:
'@smithy/fetch-http-handler': 5.3.9
'@smithy/node-http-handler': 4.4.9
'@smithy/types': 4.12.0
'@smithy/util-base64': 4.3.0
'@smithy/util-buffer-from': 4.2.0
'@smithy/util-hex-encoding': 4.2.0
'@smithy/util-utf8': 4.2.0
tslib: 2.8.1
'@smithy/util-uri-escape@4.0.0': '@smithy/util-uri-escape@4.0.0':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
'@smithy/util-uri-escape@4.2.0':
dependencies:
tslib: 2.8.1
'@smithy/util-utf8@2.3.0': '@smithy/util-utf8@2.3.0':
dependencies: dependencies:
'@smithy/util-buffer-from': 2.2.0 '@smithy/util-buffer-from': 2.2.0
@@ -11943,21 +11661,12 @@ snapshots:
'@smithy/util-buffer-from': 4.0.0 '@smithy/util-buffer-from': 4.0.0
tslib: 2.8.1 tslib: 2.8.1
'@smithy/util-utf8@4.2.0':
dependencies:
'@smithy/util-buffer-from': 4.2.0
tslib: 2.8.1
'@smithy/util-waiter@4.0.7': '@smithy/util-waiter@4.0.7':
dependencies: dependencies:
'@smithy/abort-controller': 4.0.5 '@smithy/abort-controller': 4.0.5
'@smithy/types': 4.3.2 '@smithy/types': 4.3.2
tslib: 2.8.1 tslib: 2.8.1
'@smithy/uuid@1.1.0':
dependencies:
tslib: 2.8.1
'@socket.io/component-emitter@3.1.2': {} '@socket.io/component-emitter@3.1.2': {}
'@standard-schema/spec@1.0.0': {} '@standard-schema/spec@1.0.0': {}
@@ -13081,11 +12790,6 @@ snapshots:
buffer-from@1.1.2: {} buffer-from@1.1.2: {}
buffer@5.6.0:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
buffer@5.7.1: buffer@5.7.1:
dependencies: dependencies:
base64-js: 1.5.1 base64-js: 1.5.1
@@ -17206,11 +16910,6 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
internal-slot: 1.1.0 internal-slot: 1.1.0
stream-browserify@3.0.0:
dependencies:
inherits: 2.0.4
readable-stream: 3.6.2
string-width@4.2.3: string-width@4.2.3:
dependencies: dependencies:
emoji-regex: 8.0.0 emoji-regex: 8.0.0