mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-19 10:09:57 -06:00
chore: introduce new reliable cache for enterprise license check (#5740)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
22
.github/workflows/e2e.yml
vendored
22
.github/workflows/e2e.yml
vendored
@@ -11,6 +11,8 @@ on:
|
||||
required: false
|
||||
PLAYWRIGHT_SERVICE_URL:
|
||||
required: false
|
||||
ENTERPRISE_LICENSE_KEY:
|
||||
required: true
|
||||
# Add other secrets if necessary
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -48,15 +50,17 @@ jobs:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
egress-policy: allow
|
||||
allowed-endpoints: |
|
||||
ee.formbricks.com:443
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
|
||||
- name: Setup Node.js 20.x
|
||||
- name: Setup Node.js 22.x
|
||||
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2
|
||||
with:
|
||||
node-version: 20.x
|
||||
node-version: 22.x
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
@@ -75,7 +79,7 @@ jobs:
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${RANDOM_KEY}/" .env
|
||||
sed -i "s/CRON_SECRET=.*/CRON_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/NEXTAUTH_SECRET=.*/NEXTAUTH_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/ENTERPRISE_LICENSE_KEY=.*/ENTERPRISE_LICENSE_KEY=${RANDOM_KEY}/" .env
|
||||
sed -i "s/ENTERPRISE_LICENSE_KEY=.*/ENTERPRISE_LICENSE_KEY=${{ secrets.ENTERPRISE_LICENSE_KEY }}/" .env
|
||||
echo "" >> .env
|
||||
echo "E2E_TESTING=1" >> .env
|
||||
shell: bash
|
||||
@@ -89,8 +93,18 @@ jobs:
|
||||
# pnpm prisma migrate deploy
|
||||
pnpm db:migrate:dev
|
||||
|
||||
- name: Check for Enterprise License
|
||||
run: |
|
||||
LICENSE_KEY=$(grep '^ENTERPRISE_LICENSE_KEY=' .env | cut -d'=' -f2-)
|
||||
if [ -z "$LICENSE_KEY" ]; then
|
||||
echo "::error::ENTERPRISE_LICENSE_KEY in .env is empty. Please check your secret configuration."
|
||||
exit 1
|
||||
fi
|
||||
echo "License key length: ${#LICENSE_KEY}"
|
||||
|
||||
- name: Run App
|
||||
run: |
|
||||
echo "Starting app with enterprise license..."
|
||||
NODE_ENV=test pnpm start --filter=@formbricks/web | tee app.log 2>&1 &
|
||||
sleep 10 # Optional: gives some buffer for the app to start
|
||||
for attempt in {1..10}; do
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import { getOrganizationsByUserId } from "@/lib/organization/service";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import Page from "./page";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
vi.mock("@/modules/ee/license-check/lib/license", () => ({
|
||||
getEnterpriseLicense: vi.fn().mockResolvedValue({
|
||||
active: true,
|
||||
features: { isMultiOrgEnabled: true },
|
||||
lastChecked: new Date(),
|
||||
isPendingDowngrade: false,
|
||||
fallbackLevel: "live",
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
@@ -97,23 +105,44 @@ vi.mock("@/app/(app)/(onboarding)/organizations/[organizationId]/landing/compone
|
||||
vi.mock("@/modules/organization/lib/utils");
|
||||
vi.mock("@/lib/user/service");
|
||||
vi.mock("@/lib/organization/service");
|
||||
vi.mock("@/modules/ee/license-check/lib/utils");
|
||||
vi.mock("@/tolgee/server");
|
||||
vi.mock("next/navigation", () => ({
|
||||
redirect: vi.fn(() => "REDIRECT_STUB"),
|
||||
notFound: vi.fn(() => "NOT_FOUND_STUB"),
|
||||
}));
|
||||
|
||||
// Mock the React cache function
|
||||
vi.mock("react", async () => {
|
||||
const actual = await vi.importActual("react");
|
||||
return {
|
||||
...actual,
|
||||
cache: (fn: any) => fn,
|
||||
};
|
||||
});
|
||||
|
||||
describe("Page component", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
test("redirects to login if no user session", async () => {
|
||||
vi.mocked(getOrganizationAuth).mockResolvedValue({ session: {}, organization: {} } as any);
|
||||
|
||||
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
|
||||
getEnterpriseLicense: vi.fn().mockResolvedValue({
|
||||
active: true,
|
||||
features: { isMultiOrgEnabled: true },
|
||||
lastChecked: new Date(),
|
||||
isPendingDowngrade: false,
|
||||
fallbackLevel: "live",
|
||||
}),
|
||||
}));
|
||||
const { default: Page } = await import("./page");
|
||||
const result = await Page({ params: { organizationId: "org1" } });
|
||||
|
||||
expect(redirect).toHaveBeenCalledWith("/auth/login");
|
||||
expect(result).toBe("REDIRECT_STUB");
|
||||
});
|
||||
@@ -124,9 +153,17 @@ describe("Page component", () => {
|
||||
organization: {},
|
||||
} as any);
|
||||
vi.mocked(getUser).mockResolvedValue(null);
|
||||
|
||||
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
|
||||
getEnterpriseLicense: vi.fn().mockResolvedValue({
|
||||
active: true,
|
||||
features: { isMultiOrgEnabled: true },
|
||||
lastChecked: new Date(),
|
||||
isPendingDowngrade: false,
|
||||
fallbackLevel: "live",
|
||||
}),
|
||||
}));
|
||||
const { default: Page } = await import("./page");
|
||||
const result = await Page({ params: { organizationId: "org1" } });
|
||||
|
||||
expect(notFound).toHaveBeenCalled();
|
||||
expect(result).toBe("NOT_FOUND_STUB");
|
||||
});
|
||||
@@ -138,14 +175,21 @@ describe("Page component", () => {
|
||||
} as any);
|
||||
vi.mocked(getUser).mockResolvedValue({ id: "user1", name: "Test User" } as any);
|
||||
vi.mocked(getOrganizationsByUserId).mockResolvedValue([{ id: "org1", name: "Org One" } as any]);
|
||||
vi.mocked(getEnterpriseLicense).mockResolvedValue({ features: { isMultiOrgEnabled: true } } as any);
|
||||
vi.mocked(getTranslate).mockResolvedValue((props: any) =>
|
||||
typeof props === "string" ? props : props.key || ""
|
||||
);
|
||||
|
||||
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
|
||||
getEnterpriseLicense: vi.fn().mockResolvedValue({
|
||||
active: true,
|
||||
features: { isMultiOrgEnabled: true },
|
||||
lastChecked: new Date(),
|
||||
isPendingDowngrade: false,
|
||||
fallbackLevel: "live",
|
||||
}),
|
||||
}));
|
||||
const { default: Page } = await import("./page");
|
||||
const element = await Page({ params: { organizationId: "org1" } });
|
||||
render(element as React.ReactElement);
|
||||
|
||||
expect(screen.getByTestId("landing-sidebar")).toBeInTheDocument();
|
||||
expect(screen.getByText("organizations.landing.no_projects_warning_title")).toBeInTheDocument();
|
||||
expect(screen.getByText("organizations.landing.no_projects_warning_subtitle")).toBeInTheDocument();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LandingSidebar } from "@/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar";
|
||||
import { getOrganizationsByUserId } from "@/lib/organization/service";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/license";
|
||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||
import { Header } from "@/modules/ui/components/header";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
|
||||
@@ -65,7 +65,7 @@ const Page = async (props: ProjectSettingsPageProps) => {
|
||||
/>
|
||||
{projects.length >= 1 && (
|
||||
<Button
|
||||
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
variant="ghost"
|
||||
asChild>
|
||||
<Link href={"/"}>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { EnvironmentLayout } from "@/app/(app)/environments/[environmentId]/components/EnvironmentLayout";
|
||||
import { getEnvironment, getEnvironments } from "@/lib/environment/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||
import { getAccessFlags } from "@/lib/membership/utils";
|
||||
@@ -10,7 +9,7 @@ import {
|
||||
} from "@/lib/organization/service";
|
||||
import { getUserProjects } from "@/lib/project/service";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import { getEnterpriseLicense, getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import type { Session } from "next-auth";
|
||||
@@ -49,7 +48,6 @@ vi.mock("@/lib/membership/utils", () => ({
|
||||
getAccessFlags: vi.fn(() => ({ isMember: true })), // Default to member for simplicity
|
||||
}));
|
||||
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
|
||||
getEnterpriseLicense: vi.fn(),
|
||||
getOrganizationProjectsLimit: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/modules/ee/teams/lib/roles", () => ({
|
||||
@@ -176,7 +174,6 @@ describe("EnvironmentLayout", () => {
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValue(mockMembership);
|
||||
vi.mocked(getMonthlyActiveOrganizationPeopleCount).mockResolvedValue(100);
|
||||
vi.mocked(getMonthlyOrganizationResponseCount).mockResolvedValue(500);
|
||||
vi.mocked(getEnterpriseLicense).mockResolvedValue(mockLicense);
|
||||
vi.mocked(getOrganizationProjectsLimit).mockResolvedValue(null as any);
|
||||
vi.mocked(getProjectPermissionByUserId).mockResolvedValue(mockProjectPermission);
|
||||
mockIsDevelopment = false;
|
||||
@@ -189,13 +186,19 @@ describe("EnvironmentLayout", () => {
|
||||
});
|
||||
|
||||
test("renders correctly with default props", async () => {
|
||||
// Ensure the default mockLicense has isPendingDowngrade: false and active: false
|
||||
vi.mocked(getEnterpriseLicense).mockResolvedValue({
|
||||
...mockLicense,
|
||||
isPendingDowngrade: false,
|
||||
active: false,
|
||||
});
|
||||
|
||||
vi.resetModules();
|
||||
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
|
||||
getEnterpriseLicense: vi.fn().mockResolvedValue({
|
||||
active: false,
|
||||
isPendingDowngrade: false,
|
||||
features: { isMultiOrgEnabled: false },
|
||||
lastChecked: new Date(),
|
||||
fallbackLevel: "live",
|
||||
}),
|
||||
}));
|
||||
const { EnvironmentLayout } = await import(
|
||||
"@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"
|
||||
);
|
||||
render(
|
||||
await EnvironmentLayout({
|
||||
environmentId: "env-1",
|
||||
@@ -203,20 +206,31 @@ describe("EnvironmentLayout", () => {
|
||||
children: <div>Child Content</div>,
|
||||
})
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("main-navigation")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("top-control-bar")).toBeInTheDocument();
|
||||
expect(screen.getByText("Child Content")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("dev-banner")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("limits-banner")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("downgrade-banner")).not.toBeInTheDocument(); // This should now pass
|
||||
expect(screen.queryByTestId("downgrade-banner")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders DevEnvironmentBanner in development environment", async () => {
|
||||
const devEnvironment = { ...mockEnvironment, type: "development" as const };
|
||||
vi.mocked(getEnvironment).mockResolvedValue(devEnvironment);
|
||||
mockIsDevelopment = true;
|
||||
|
||||
vi.resetModules();
|
||||
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
|
||||
getEnterpriseLicense: vi.fn().mockResolvedValue({
|
||||
active: false,
|
||||
isPendingDowngrade: false,
|
||||
features: { isMultiOrgEnabled: false },
|
||||
lastChecked: new Date(),
|
||||
fallbackLevel: "live",
|
||||
}),
|
||||
}));
|
||||
const { EnvironmentLayout } = await import(
|
||||
"@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"
|
||||
);
|
||||
render(
|
||||
await EnvironmentLayout({
|
||||
environmentId: "env-1",
|
||||
@@ -224,13 +238,24 @@ describe("EnvironmentLayout", () => {
|
||||
children: <div>Child Content</div>,
|
||||
})
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("dev-banner")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders LimitsReachedBanner in Formbricks Cloud", async () => {
|
||||
mockIsFormbricksCloud = true;
|
||||
|
||||
vi.resetModules();
|
||||
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
|
||||
getEnterpriseLicense: vi.fn().mockResolvedValue({
|
||||
active: false,
|
||||
isPendingDowngrade: false,
|
||||
features: { isMultiOrgEnabled: false },
|
||||
lastChecked: new Date(),
|
||||
fallbackLevel: "live",
|
||||
}),
|
||||
}));
|
||||
const { EnvironmentLayout } = await import(
|
||||
"@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"
|
||||
);
|
||||
render(
|
||||
await EnvironmentLayout({
|
||||
environmentId: "env-1",
|
||||
@@ -238,17 +263,21 @@ describe("EnvironmentLayout", () => {
|
||||
children: <div>Child Content</div>,
|
||||
})
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("limits-banner")).toBeInTheDocument();
|
||||
expect(vi.mocked(getMonthlyActiveOrganizationPeopleCount)).toHaveBeenCalledWith(mockOrganization.id);
|
||||
expect(vi.mocked(getMonthlyOrganizationResponseCount)).toHaveBeenCalledWith(mockOrganization.id);
|
||||
});
|
||||
|
||||
test("renders PendingDowngradeBanner when pending downgrade", async () => {
|
||||
// Ensure the license mock reflects the condition needed for the banner
|
||||
const pendingLicense = { ...mockLicense, isPendingDowngrade: true, active: true };
|
||||
vi.mocked(getEnterpriseLicense).mockResolvedValue(pendingLicense);
|
||||
|
||||
vi.mocked(getOrganizationProjectsLimit).mockResolvedValue(null as any);
|
||||
vi.resetModules();
|
||||
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
|
||||
getEnterpriseLicense: vi.fn().mockResolvedValue(pendingLicense),
|
||||
}));
|
||||
const { EnvironmentLayout } = await import(
|
||||
"@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"
|
||||
);
|
||||
render(
|
||||
await EnvironmentLayout({
|
||||
environmentId: "env-1",
|
||||
@@ -256,12 +285,24 @@ describe("EnvironmentLayout", () => {
|
||||
children: <div>Child Content</div>,
|
||||
})
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("downgrade-banner")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("throws error if user not found", async () => {
|
||||
vi.mocked(getUser).mockResolvedValue(null);
|
||||
vi.resetModules();
|
||||
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
|
||||
getEnterpriseLicense: vi.fn().mockResolvedValue({
|
||||
active: false,
|
||||
isPendingDowngrade: false,
|
||||
features: { isMultiOrgEnabled: false },
|
||||
lastChecked: new Date(),
|
||||
fallbackLevel: "live",
|
||||
}),
|
||||
}));
|
||||
const { EnvironmentLayout } = await import(
|
||||
"@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"
|
||||
);
|
||||
await expect(EnvironmentLayout({ environmentId: "env-1", session: mockSession })).rejects.toThrow(
|
||||
"common.user_not_found"
|
||||
);
|
||||
@@ -269,6 +310,19 @@ describe("EnvironmentLayout", () => {
|
||||
|
||||
test("throws error if organization not found", async () => {
|
||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(null);
|
||||
vi.resetModules();
|
||||
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
|
||||
getEnterpriseLicense: vi.fn().mockResolvedValue({
|
||||
active: false,
|
||||
isPendingDowngrade: false,
|
||||
features: { isMultiOrgEnabled: false },
|
||||
lastChecked: new Date(),
|
||||
fallbackLevel: "live",
|
||||
}),
|
||||
}));
|
||||
const { EnvironmentLayout } = await import(
|
||||
"@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"
|
||||
);
|
||||
await expect(EnvironmentLayout({ environmentId: "env-1", session: mockSession })).rejects.toThrow(
|
||||
"common.organization_not_found"
|
||||
);
|
||||
@@ -276,13 +330,39 @@ describe("EnvironmentLayout", () => {
|
||||
|
||||
test("throws error if environment not found", async () => {
|
||||
vi.mocked(getEnvironment).mockResolvedValue(null);
|
||||
vi.resetModules();
|
||||
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
|
||||
getEnterpriseLicense: vi.fn().mockResolvedValue({
|
||||
active: false,
|
||||
isPendingDowngrade: false,
|
||||
features: { isMultiOrgEnabled: false },
|
||||
lastChecked: new Date(),
|
||||
fallbackLevel: "live",
|
||||
}),
|
||||
}));
|
||||
const { EnvironmentLayout } = await import(
|
||||
"@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"
|
||||
);
|
||||
await expect(EnvironmentLayout({ environmentId: "env-1", session: mockSession })).rejects.toThrow(
|
||||
"common.environment_not_found"
|
||||
);
|
||||
});
|
||||
|
||||
test("throws error if projects, environments or organizations not found", async () => {
|
||||
vi.mocked(getUserProjects).mockResolvedValue(null as any); // Simulate one of the promises failing
|
||||
vi.mocked(getUserProjects).mockResolvedValue(null as any);
|
||||
vi.resetModules();
|
||||
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
|
||||
getEnterpriseLicense: vi.fn().mockResolvedValue({
|
||||
active: false,
|
||||
isPendingDowngrade: false,
|
||||
features: { isMultiOrgEnabled: false },
|
||||
lastChecked: new Date(),
|
||||
fallbackLevel: "live",
|
||||
}),
|
||||
}));
|
||||
const { EnvironmentLayout } = await import(
|
||||
"@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"
|
||||
);
|
||||
await expect(EnvironmentLayout({ environmentId: "env-1", session: mockSession })).rejects.toThrow(
|
||||
"environments.projects_environments_organizations_not_found"
|
||||
);
|
||||
@@ -291,6 +371,19 @@ describe("EnvironmentLayout", () => {
|
||||
test("throws error if member has no project permission", async () => {
|
||||
vi.mocked(getAccessFlags).mockReturnValue({ isMember: true } as any);
|
||||
vi.mocked(getProjectPermissionByUserId).mockResolvedValue(null);
|
||||
vi.resetModules();
|
||||
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
|
||||
getEnterpriseLicense: vi.fn().mockResolvedValue({
|
||||
active: false,
|
||||
isPendingDowngrade: false,
|
||||
features: { isMultiOrgEnabled: false },
|
||||
lastChecked: new Date(),
|
||||
fallbackLevel: "live",
|
||||
}),
|
||||
}));
|
||||
const { EnvironmentLayout } = await import(
|
||||
"@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"
|
||||
);
|
||||
await expect(EnvironmentLayout({ environmentId: "env-1", session: mockSession })).rejects.toThrow(
|
||||
"common.project_permission_not_found"
|
||||
);
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
} from "@/lib/organization/service";
|
||||
import { getUserProjects } from "@/lib/project/service";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import { getEnterpriseLicense, getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/license";
|
||||
import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner";
|
||||
import { LimitsReachedBanner } from "@/modules/ui/components/limits-reached-banner";
|
||||
|
||||
@@ -10,7 +10,6 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TMembership } from "@formbricks/types/memberships";
|
||||
import { TOrganization, TOrganizationBilling } from "@formbricks/types/organizations";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import EnterpriseSettingsPage from "./page";
|
||||
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
@@ -179,15 +178,23 @@ describe("EnterpriseSettingsPage", () => {
|
||||
});
|
||||
|
||||
test("renders correctly for an owner when not on Formbricks Cloud", async () => {
|
||||
vi.resetModules();
|
||||
await vi.doMock("@/modules/ee/license-check/lib/license", () => ({
|
||||
getEnterpriseLicense: vi.fn().mockResolvedValue({
|
||||
active: false,
|
||||
isPendingDowngrade: false,
|
||||
features: { isMultiOrgEnabled: false },
|
||||
lastChecked: new Date(),
|
||||
fallbackLevel: "live",
|
||||
}),
|
||||
}));
|
||||
const { default: EnterpriseSettingsPage } = await import("./page");
|
||||
const Page = await EnterpriseSettingsPage({ params: { environmentId: mockEnvironmentId } });
|
||||
render(Page);
|
||||
|
||||
expect(screen.getByTestId("page-content-wrapper")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("page-header")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("environments.settings.enterprise.sso")).toBeInTheDocument();
|
||||
expect(screen.getByText("environments.settings.billing.remove_branding")).toBeInTheDocument();
|
||||
|
||||
expect(redirect).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/license";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
@@ -118,7 +118,7 @@ const Page = async (props) => {
|
||||
<div className="relative isolate mt-8 overflow-hidden rounded-lg bg-slate-900 px-3 pt-8 shadow-2xl sm:px-8 md:pt-12 lg:flex lg:gap-x-10 lg:px-12 lg:pt-0">
|
||||
<svg
|
||||
viewBox="0 0 1024 1024"
|
||||
className="absolute top-1/2 left-1/2 -z-10 h-[64rem] w-[64rem] -translate-y-1/2 [mask-image:radial-gradient(closest-side,white,transparent)] sm:left-full sm:-ml-80 lg:left-1/2 lg:ml-0 lg:-translate-x-1/2 lg:translate-y-0"
|
||||
className="absolute left-1/2 top-1/2 -z-10 h-[64rem] w-[64rem] -translate-y-1/2 [mask-image:radial-gradient(closest-side,white,transparent)] sm:left-full sm:-ml-80 lg:left-1/2 lg:ml-0 lg:-translate-x-1/2 lg:translate-y-0"
|
||||
aria-hidden="true">
|
||||
<circle
|
||||
cx={512}
|
||||
|
||||
@@ -56,6 +56,10 @@ vi.mock("@tolgee/react", async () => {
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
fatal: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import { Testimonial } from "@/modules/auth/components/testimonial";
|
||||
import {
|
||||
getIsMultiOrgEnabled,
|
||||
getIsSamlSsoEnabled,
|
||||
getisSsoEnabled,
|
||||
getIsSsoEnabled,
|
||||
} from "@/modules/ee/license-check/lib/utils";
|
||||
import { Metadata } from "next";
|
||||
import { LoginForm } from "./components/login-form";
|
||||
@@ -29,7 +29,7 @@ export const metadata: Metadata = {
|
||||
export const LoginPage = async () => {
|
||||
const [isMultiOrgEnabled, isSsoEnabled, isSamlSsoEnabled] = await Promise.all([
|
||||
getIsMultiOrgEnabled(),
|
||||
getisSsoEnabled(),
|
||||
getIsSsoEnabled(),
|
||||
getIsSamlSsoEnabled(),
|
||||
]);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getIsValidInviteToken } from "@/modules/auth/signup/lib/invite";
|
||||
import {
|
||||
getIsMultiOrgEnabled,
|
||||
getIsSamlSsoEnabled,
|
||||
getisSsoEnabled,
|
||||
getIsSsoEnabled,
|
||||
} from "@/modules/ee/license-check/lib/utils";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
@@ -29,8 +29,14 @@ vi.mock("@/modules/auth/signup/components/signup-form", () => ({
|
||||
|
||||
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
|
||||
getIsMultiOrgEnabled: vi.fn(),
|
||||
getIsSsoEnabled: vi.fn(),
|
||||
getIsSamlSsoEnabled: vi.fn(),
|
||||
getisSsoEnabled: vi.fn(),
|
||||
getIsWhitelabelEnabled: vi.fn(),
|
||||
getIsRemoveBrandingEnabled: vi.fn(),
|
||||
getIsContactsEnabled: vi.fn(),
|
||||
getIsAiEnabled: vi.fn(),
|
||||
getIsSpamProtectionEnabled: vi.fn(),
|
||||
getIsPendingDowngrade: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/auth/signup/lib/invite", () => ({
|
||||
@@ -113,7 +119,7 @@ describe("SignupPage", () => {
|
||||
test("renders the signup page with all components when signup is enabled", async () => {
|
||||
// Mock the license check functions to return true
|
||||
vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true);
|
||||
vi.mocked(getisSsoEnabled).mockResolvedValue(true);
|
||||
vi.mocked(getIsSsoEnabled).mockResolvedValue(true);
|
||||
vi.mocked(getIsSamlSsoEnabled).mockResolvedValue(true);
|
||||
vi.mocked(findMatchingLocale).mockResolvedValue("en-US");
|
||||
vi.mocked(verifyInviteToken).mockReturnValue({
|
||||
@@ -172,7 +178,7 @@ describe("SignupPage", () => {
|
||||
test("renders the page with email from search params", async () => {
|
||||
// Mock the license check functions to return true
|
||||
vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true);
|
||||
vi.mocked(getisSsoEnabled).mockResolvedValue(true);
|
||||
vi.mocked(getIsSsoEnabled).mockResolvedValue(true);
|
||||
vi.mocked(getIsSamlSsoEnabled).mockResolvedValue(true);
|
||||
vi.mocked(findMatchingLocale).mockResolvedValue("en-US");
|
||||
vi.mocked(verifyInviteToken).mockReturnValue({
|
||||
|
||||
@@ -24,7 +24,7 @@ import { getIsValidInviteToken } from "@/modules/auth/signup/lib/invite";
|
||||
import {
|
||||
getIsMultiOrgEnabled,
|
||||
getIsSamlSsoEnabled,
|
||||
getisSsoEnabled,
|
||||
getIsSsoEnabled,
|
||||
} from "@/modules/ee/license-check/lib/utils";
|
||||
import { notFound } from "next/navigation";
|
||||
import { SignupForm } from "./components/signup-form";
|
||||
@@ -34,7 +34,7 @@ export const SignupPage = async ({ searchParams: searchParamsProps }) => {
|
||||
const inviteToken = searchParams["inviteToken"] ?? null;
|
||||
const [isMultOrgEnabled, isSsoEnabled, isSamlSsoEnabled] = await Promise.all([
|
||||
getIsMultiOrgEnabled(),
|
||||
getisSsoEnabled(),
|
||||
getIsSsoEnabled(),
|
||||
getIsSamlSsoEnabled(),
|
||||
]);
|
||||
|
||||
|
||||
117
apps/web/modules/cache/lib/service.test.ts
vendored
Normal file
117
apps/web/modules/cache/lib/service.test.ts
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
import KeyvRedis from "@keyv/redis";
|
||||
import { createCache } from "cache-manager";
|
||||
import { Keyv } from "keyv";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { logger } from "@formbricks/logger";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("keyv");
|
||||
vi.mock("@keyv/redis");
|
||||
vi.mock("cache-manager");
|
||||
vi.mock("@formbricks/logger");
|
||||
|
||||
const mockCacheInstance = {
|
||||
set: vi.fn(),
|
||||
get: vi.fn(),
|
||||
del: vi.fn(),
|
||||
};
|
||||
|
||||
const CACHE_TTL_SECONDS = 60 * 60 * 24; // 24 hours
|
||||
const CACHE_TTL_MS = CACHE_TTL_SECONDS * 1000;
|
||||
|
||||
describe("Cache Service", () => {
|
||||
let originalRedisUrl: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
originalRedisUrl = process.env.REDIS_URL;
|
||||
vi.resetAllMocks();
|
||||
vi.resetModules(); // Crucial for re-running module initialization logic
|
||||
|
||||
// Setup default mock implementations
|
||||
vi.mocked(createCache).mockReturnValue(mockCacheInstance as any);
|
||||
vi.mocked(Keyv).mockClear(); // Clear any previous calls
|
||||
vi.mocked(KeyvRedis).mockClear(); // Clear any previous calls
|
||||
vi.mocked(logger.warn).mockClear(); // Clear logger warnings
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.REDIS_URL = originalRedisUrl;
|
||||
});
|
||||
|
||||
describe("Initialization and getCache", () => {
|
||||
test("should use Redis store and return it via getCache if REDIS_URL is set", async () => {
|
||||
process.env.REDIS_URL = "redis://localhost:6379";
|
||||
const { getCache } = await import("./service"); // Dynamically import
|
||||
|
||||
expect(KeyvRedis).toHaveBeenCalledWith("redis://localhost:6379");
|
||||
expect(Keyv).toHaveBeenCalledWith({
|
||||
store: expect.any(KeyvRedis),
|
||||
ttl: CACHE_TTL_MS,
|
||||
});
|
||||
expect(createCache).toHaveBeenCalledWith({
|
||||
stores: [expect.any(Keyv)],
|
||||
ttl: CACHE_TTL_MS,
|
||||
});
|
||||
expect(logger.warn).not.toHaveBeenCalled();
|
||||
expect(logger.info).toHaveBeenCalledWith("Successfully connected to Redis cache");
|
||||
expect(getCache()).toBe(mockCacheInstance);
|
||||
});
|
||||
|
||||
test("should fall back to memory store if Redis connection fails", async () => {
|
||||
process.env.REDIS_URL = "redis://localhost:6379";
|
||||
const mockError = new Error("Connection refused");
|
||||
vi.mocked(KeyvRedis).mockImplementation(() => {
|
||||
throw mockError;
|
||||
});
|
||||
|
||||
const { getCache } = await import("./service"); // Dynamically import
|
||||
|
||||
expect(KeyvRedis).toHaveBeenCalledWith("redis://localhost:6379");
|
||||
expect(logger.error).toHaveBeenCalledWith("Failed to connect to Redis cache:", mockError);
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
"Falling back to in-memory cache due to Redis connection failure"
|
||||
);
|
||||
expect(Keyv).toHaveBeenCalledWith({
|
||||
ttl: CACHE_TTL_MS,
|
||||
});
|
||||
expect(createCache).toHaveBeenCalledWith({
|
||||
stores: [expect.any(Keyv)],
|
||||
ttl: CACHE_TTL_MS,
|
||||
});
|
||||
expect(getCache()).toBe(mockCacheInstance);
|
||||
});
|
||||
|
||||
test("should use memory store, log warning, and return it via getCache if REDIS_URL is not set", async () => {
|
||||
delete process.env.REDIS_URL;
|
||||
const { getCache } = await import("./service"); // Dynamically import
|
||||
|
||||
expect(KeyvRedis).not.toHaveBeenCalled();
|
||||
expect(Keyv).toHaveBeenCalledWith({
|
||||
ttl: CACHE_TTL_MS,
|
||||
});
|
||||
expect(createCache).toHaveBeenCalledWith({
|
||||
stores: [expect.any(Keyv)],
|
||||
ttl: CACHE_TTL_MS,
|
||||
});
|
||||
expect(logger.warn).toHaveBeenCalledWith("REDIS_URL not found, falling back to in-memory cache.");
|
||||
expect(getCache()).toBe(mockCacheInstance);
|
||||
});
|
||||
|
||||
test("should use memory store, log warning, and return it via getCache if REDIS_URL is an empty string", async () => {
|
||||
process.env.REDIS_URL = ""; // Test with empty string
|
||||
const { getCache } = await import("./service"); // Dynamically import
|
||||
|
||||
// If REDIS_URL is "", it's falsy, so it should fall back to memory store
|
||||
expect(KeyvRedis).not.toHaveBeenCalled();
|
||||
expect(Keyv).toHaveBeenCalledWith({
|
||||
ttl: CACHE_TTL_MS, // Expect memory store configuration
|
||||
});
|
||||
expect(createCache).toHaveBeenCalledWith({
|
||||
stores: [expect.any(Keyv)],
|
||||
ttl: CACHE_TTL_MS,
|
||||
});
|
||||
expect(logger.warn).toHaveBeenCalledWith("REDIS_URL not found, falling back to in-memory cache.");
|
||||
expect(getCache()).toBe(mockCacheInstance);
|
||||
});
|
||||
});
|
||||
});
|
||||
55
apps/web/modules/cache/lib/service.ts
vendored
Normal file
55
apps/web/modules/cache/lib/service.ts
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
import "server-only";
|
||||
import KeyvRedis from "@keyv/redis";
|
||||
import { type Cache, createCache } from "cache-manager";
|
||||
import { Keyv } from "keyv";
|
||||
import { logger } from "@formbricks/logger";
|
||||
|
||||
const CACHE_TTL_SECONDS = 60 * 60 * 24; // 24 hours
|
||||
const CACHE_TTL_MS = CACHE_TTL_SECONDS * 1000;
|
||||
|
||||
let cache: Cache;
|
||||
|
||||
const initializeMemoryCache = (): void => {
|
||||
const memoryKeyvStore = new Keyv({
|
||||
ttl: CACHE_TTL_MS,
|
||||
});
|
||||
cache = createCache({
|
||||
stores: [memoryKeyvStore],
|
||||
ttl: CACHE_TTL_MS,
|
||||
});
|
||||
logger.info("Using in-memory cache");
|
||||
};
|
||||
|
||||
if (process.env.REDIS_URL) {
|
||||
try {
|
||||
const redisStore = new KeyvRedis(process.env.REDIS_URL);
|
||||
|
||||
// Gracefully fall back if Redis dies later on
|
||||
redisStore.on("error", (err) => {
|
||||
logger.error("Redis connection lost – switching to in-memory cache", { error: err });
|
||||
initializeMemoryCache();
|
||||
});
|
||||
|
||||
const redisKeyvStore = new Keyv({
|
||||
store: redisStore,
|
||||
ttl: CACHE_TTL_MS,
|
||||
});
|
||||
|
||||
cache = createCache({
|
||||
stores: [redisKeyvStore],
|
||||
ttl: CACHE_TTL_MS,
|
||||
});
|
||||
logger.info("Successfully connected to Redis cache");
|
||||
} catch (error) {
|
||||
logger.error("Failed to connect to Redis cache:", error);
|
||||
logger.warn("Falling back to in-memory cache due to Redis connection failure");
|
||||
initializeMemoryCache();
|
||||
}
|
||||
} else {
|
||||
logger.warn("REDIS_URL not found, falling back to in-memory cache.");
|
||||
initializeMemoryCache();
|
||||
}
|
||||
|
||||
export const getCache = (): Cache => {
|
||||
return cache;
|
||||
};
|
||||
475
apps/web/modules/ee/license-check/lib/license.test.ts
Normal file
475
apps/web/modules/ee/license-check/lib/license.test.ts
Normal file
@@ -0,0 +1,475 @@
|
||||
import {
|
||||
TEnterpriseLicenseDetails,
|
||||
TEnterpriseLicenseFeatures,
|
||||
} from "@/modules/ee/license-check/types/enterprise-license";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import type { Mock } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
|
||||
// Mock declarations must be at the top level
|
||||
vi.mock("@/lib/env", () => ({
|
||||
env: {
|
||||
ENTERPRISE_LICENSE_KEY: "test-license-key",
|
||||
VERCEL_URL: "some.vercel.url",
|
||||
FORMBRICKS_COM_URL: "https://app.formbricks.com",
|
||||
HTTPS_PROXY: undefined,
|
||||
HTTP_PROXY: undefined,
|
||||
},
|
||||
}));
|
||||
|
||||
const mockCache = {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
del: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
store: { name: "memory" },
|
||||
};
|
||||
|
||||
vi.mock("@/modules/cache/lib/service", () => ({
|
||||
getCache: () => mockCache,
|
||||
}));
|
||||
|
||||
vi.mock("node-fetch", () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
response: {
|
||||
count: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock constants as they are used in the original license.ts indirectly
|
||||
vi.mock("@/lib/constants", async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
...(typeof actual === "object" && actual !== null ? actual : {}),
|
||||
IS_FORMBRICKS_CLOUD: false, // Default to self-hosted for most tests
|
||||
REVALIDATION_INTERVAL: 3600, // Example value
|
||||
ENTERPRISE_LICENSE_KEY: "test-license-key",
|
||||
};
|
||||
});
|
||||
|
||||
describe("License Core Logic", () => {
|
||||
let originalProcessEnv: NodeJS.ProcessEnv;
|
||||
|
||||
beforeEach(() => {
|
||||
originalProcessEnv = { ...process.env };
|
||||
vi.resetAllMocks();
|
||||
mockCache.get.mockReset();
|
||||
mockCache.set.mockReset();
|
||||
mockCache.del.mockReset();
|
||||
vi.mocked(prisma.response.count).mockResolvedValue(100);
|
||||
vi.clearAllMocks();
|
||||
// Mock window to be undefined for server-side tests
|
||||
vi.stubGlobal("window", undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalProcessEnv;
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("getEnterpriseLicense", () => {
|
||||
const mockFetchedLicenseDetailsFeatures: TEnterpriseLicenseFeatures = {
|
||||
isMultiOrgEnabled: true,
|
||||
contacts: true,
|
||||
projects: 10,
|
||||
whitelabel: true,
|
||||
removeBranding: true,
|
||||
twoFactorAuth: true,
|
||||
sso: true,
|
||||
saml: true,
|
||||
spamProtection: true,
|
||||
ai: false,
|
||||
};
|
||||
const mockFetchedLicenseDetails: TEnterpriseLicenseDetails = {
|
||||
status: "active",
|
||||
features: mockFetchedLicenseDetailsFeatures,
|
||||
};
|
||||
|
||||
const expectedActiveLicenseState = {
|
||||
active: true,
|
||||
features: mockFetchedLicenseDetails.features,
|
||||
lastChecked: expect.any(Date),
|
||||
isPendingDowngrade: false,
|
||||
fallbackLevel: "live" as const,
|
||||
};
|
||||
|
||||
test("should return cached license from FETCH_LICENSE_CACHE_KEY if available and valid", async () => {
|
||||
const { getEnterpriseLicense } = await import("./license");
|
||||
const fetch = (await import("node-fetch")).default as Mock;
|
||||
|
||||
mockCache.get.mockImplementation(async (key) => {
|
||||
if (key.startsWith("formbricksEnterpriseLicense-details")) {
|
||||
return mockFetchedLicenseDetails;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const license = await getEnterpriseLicense();
|
||||
expect(license).toEqual(expectedActiveLicenseState);
|
||||
expect(mockCache.get).toHaveBeenCalledWith(
|
||||
expect.stringContaining("formbricksEnterpriseLicense-details")
|
||||
);
|
||||
expect(fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should fetch license if not in FETCH_LICENSE_CACHE_KEY", async () => {
|
||||
const { getEnterpriseLicense } = await import("./license");
|
||||
const fetch = (await import("node-fetch")).default as Mock;
|
||||
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
(fetch as Mock).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ data: mockFetchedLicenseDetails }),
|
||||
} as any);
|
||||
|
||||
const license = await getEnterpriseLicense();
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(mockCache.set).toHaveBeenCalledWith(
|
||||
expect.stringContaining("formbricksEnterpriseLicense-details"),
|
||||
mockFetchedLicenseDetails,
|
||||
expect.any(Number)
|
||||
);
|
||||
expect(mockCache.set).toHaveBeenCalledWith(
|
||||
expect.stringContaining("formbricksEnterpriseLicense-previousResult"),
|
||||
{
|
||||
active: true,
|
||||
features: mockFetchedLicenseDetails.features,
|
||||
lastChecked: expect.any(Date),
|
||||
version: 1,
|
||||
},
|
||||
expect.any(Number)
|
||||
);
|
||||
expect(license).toEqual(expectedActiveLicenseState);
|
||||
});
|
||||
|
||||
test("should use previous result if fetch fails and previous result exists and is within grace period", async () => {
|
||||
const { getEnterpriseLicense } = await import("./license");
|
||||
const fetch = (await import("node-fetch")).default as Mock;
|
||||
|
||||
const previousTime = new Date(Date.now() - 1 * 24 * 60 * 60 * 1000); // 1 day ago, within grace period
|
||||
const mockPreviousResult = {
|
||||
active: true,
|
||||
features: { removeBranding: true, projects: 5 },
|
||||
lastChecked: previousTime,
|
||||
version: 1,
|
||||
};
|
||||
mockCache.get.mockImplementation(async (key) => {
|
||||
if (key.startsWith("formbricksEnterpriseLicense-details")) return null;
|
||||
if (key.startsWith("formbricksEnterpriseLicense-previousResult")) return mockPreviousResult;
|
||||
return null;
|
||||
});
|
||||
(fetch as Mock).mockResolvedValueOnce({ ok: false, status: 500 } as any);
|
||||
|
||||
const license = await getEnterpriseLicense();
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(license).toEqual({
|
||||
active: true,
|
||||
features: mockPreviousResult.features,
|
||||
lastChecked: previousTime,
|
||||
isPendingDowngrade: true,
|
||||
fallbackLevel: "grace" as const,
|
||||
});
|
||||
});
|
||||
|
||||
test("should return inactive and set new previousResult if fetch fails and previous result is outside grace period", async () => {
|
||||
const { getEnterpriseLicense } = await import("./license");
|
||||
const fetch = (await import("node-fetch")).default as Mock;
|
||||
|
||||
const previousTime = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000); // 5 days ago, outside grace period
|
||||
const mockPreviousResult = {
|
||||
active: true,
|
||||
features: { removeBranding: true },
|
||||
lastChecked: previousTime,
|
||||
version: 1,
|
||||
};
|
||||
mockCache.get.mockImplementation(async (key) => {
|
||||
if (key.startsWith("formbricksEnterpriseLicense-details")) return null;
|
||||
if (key.startsWith("formbricksEnterpriseLicense-previousResult")) return mockPreviousResult;
|
||||
return null;
|
||||
});
|
||||
(fetch as Mock).mockResolvedValueOnce({ ok: false, status: 500 } as any);
|
||||
|
||||
const license = await getEnterpriseLicense();
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect(mockCache.set).toHaveBeenCalledWith(
|
||||
expect.stringContaining("formbricksEnterpriseLicense-previousResult"),
|
||||
{
|
||||
active: false,
|
||||
features: {
|
||||
isMultiOrgEnabled: false,
|
||||
projects: 3,
|
||||
twoFactorAuth: false,
|
||||
sso: false,
|
||||
whitelabel: false,
|
||||
removeBranding: false,
|
||||
contacts: false,
|
||||
ai: false,
|
||||
saml: false,
|
||||
spamProtection: false,
|
||||
},
|
||||
lastChecked: expect.any(Date),
|
||||
version: 1,
|
||||
},
|
||||
expect.any(Number)
|
||||
);
|
||||
expect(license).toEqual({
|
||||
active: false,
|
||||
features: {
|
||||
isMultiOrgEnabled: false,
|
||||
projects: 3,
|
||||
twoFactorAuth: false,
|
||||
sso: false,
|
||||
whitelabel: false,
|
||||
removeBranding: false,
|
||||
contacts: false,
|
||||
ai: false,
|
||||
saml: false,
|
||||
spamProtection: false,
|
||||
},
|
||||
lastChecked: expect.any(Date),
|
||||
isPendingDowngrade: false,
|
||||
fallbackLevel: "default" as const,
|
||||
});
|
||||
});
|
||||
|
||||
test("should return inactive with default features if fetch fails and no previous result (initial fail)", async () => {
|
||||
const { getEnterpriseLicense } = await import("./license");
|
||||
const fetch = (await import("node-fetch")).default as Mock;
|
||||
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
(fetch as Mock).mockRejectedValueOnce(new Error("Network error"));
|
||||
|
||||
const license = await getEnterpriseLicense();
|
||||
const expectedFeatures: TEnterpriseLicenseFeatures = {
|
||||
isMultiOrgEnabled: false,
|
||||
projects: 3,
|
||||
twoFactorAuth: false,
|
||||
sso: false,
|
||||
whitelabel: false,
|
||||
removeBranding: false,
|
||||
contacts: false,
|
||||
ai: false,
|
||||
saml: false,
|
||||
spamProtection: false,
|
||||
};
|
||||
expect(mockCache.set).toHaveBeenCalledWith(
|
||||
expect.stringContaining("formbricksEnterpriseLicense-previousResult"),
|
||||
{
|
||||
active: false,
|
||||
features: expectedFeatures,
|
||||
lastChecked: expect.any(Date),
|
||||
version: 1,
|
||||
},
|
||||
expect.any(Number)
|
||||
);
|
||||
expect(license).toEqual({
|
||||
active: false,
|
||||
features: expectedFeatures,
|
||||
lastChecked: expect.any(Date),
|
||||
isPendingDowngrade: false,
|
||||
fallbackLevel: "default" as const,
|
||||
});
|
||||
});
|
||||
|
||||
test("should return inactive license if ENTERPRISE_LICENSE_KEY is not set in env", async () => {
|
||||
// Reset all mocks first
|
||||
vi.resetAllMocks();
|
||||
mockCache.get.mockReset();
|
||||
mockCache.set.mockReset();
|
||||
const fetch = (await import("node-fetch")).default as Mock;
|
||||
fetch.mockReset();
|
||||
|
||||
// Mock the env module with empty license key
|
||||
vi.doMock("@/lib/env", () => ({
|
||||
env: {
|
||||
ENTERPRISE_LICENSE_KEY: "",
|
||||
VERCEL_URL: "some.vercel.url",
|
||||
FORMBRICKS_COM_URL: "https://app.formbricks.com",
|
||||
HTTPS_PROXY: undefined,
|
||||
HTTP_PROXY: undefined,
|
||||
},
|
||||
}));
|
||||
|
||||
// Re-import the module to apply the new mock
|
||||
const { getEnterpriseLicense } = await import("./license");
|
||||
const license = await getEnterpriseLicense();
|
||||
|
||||
expect(license).toEqual({
|
||||
active: false,
|
||||
features: null,
|
||||
lastChecked: expect.any(Date),
|
||||
isPendingDowngrade: false,
|
||||
fallbackLevel: "default" as const,
|
||||
});
|
||||
expect(mockCache.get).not.toHaveBeenCalled();
|
||||
expect(mockCache.set).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should handle fetch throwing an error and use grace period or return inactive", async () => {
|
||||
const { getEnterpriseLicense } = await import("./license");
|
||||
const fetch = (await import("node-fetch")).default as Mock;
|
||||
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
(fetch as Mock).mockRejectedValueOnce(new Error("Network error"));
|
||||
|
||||
const license = await getEnterpriseLicense();
|
||||
expect(license).toEqual({
|
||||
active: false,
|
||||
features: null,
|
||||
lastChecked: expect.any(Date),
|
||||
isPendingDowngrade: false,
|
||||
fallbackLevel: "default" as const,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getLicenseFeatures", () => {
|
||||
test("should return features if license is active", async () => {
|
||||
// Set up environment before import
|
||||
vi.stubGlobal("window", undefined);
|
||||
vi.doMock("@/lib/env", () => ({
|
||||
env: {
|
||||
ENTERPRISE_LICENSE_KEY: "test-license-key",
|
||||
VERCEL_URL: "some.vercel.url",
|
||||
FORMBRICKS_COM_URL: "https://app.formbricks.com",
|
||||
HTTPS_PROXY: undefined,
|
||||
HTTP_PROXY: undefined,
|
||||
},
|
||||
}));
|
||||
// Import hashString to compute the expected cache key
|
||||
const { hashString } = await import("@/lib/hashString");
|
||||
const hashedKey = hashString("test-license-key");
|
||||
const detailsKey = `formbricksEnterpriseLicense-details-${hashedKey}`;
|
||||
// Patch the cache mock to match the actual key logic
|
||||
mockCache.get.mockImplementation(async (key) => {
|
||||
if (key === detailsKey) {
|
||||
return {
|
||||
status: "active",
|
||||
features: {
|
||||
isMultiOrgEnabled: true,
|
||||
contacts: true,
|
||||
projects: 5,
|
||||
whitelabel: true,
|
||||
removeBranding: true,
|
||||
twoFactorAuth: true,
|
||||
sso: true,
|
||||
saml: true,
|
||||
spamProtection: true,
|
||||
ai: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
// Import after env and mocks are set
|
||||
const { getLicenseFeatures } = await import("./license");
|
||||
const features = await getLicenseFeatures();
|
||||
expect(features).toEqual({
|
||||
isMultiOrgEnabled: true,
|
||||
contacts: true,
|
||||
projects: 5,
|
||||
whitelabel: true,
|
||||
removeBranding: true,
|
||||
twoFactorAuth: true,
|
||||
sso: true,
|
||||
saml: true,
|
||||
spamProtection: true,
|
||||
ai: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("should return null if license is inactive", async () => {
|
||||
const { getLicenseFeatures } = await import("./license");
|
||||
mockCache.get.mockImplementation(async (key) => {
|
||||
if (key.startsWith("formbricksEnterpriseLicense-details")) {
|
||||
return { status: "expired", features: null };
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const features = await getLicenseFeatures();
|
||||
expect(features).toBeNull();
|
||||
});
|
||||
|
||||
test("should return null if getEnterpriseLicense throws", async () => {
|
||||
const { getLicenseFeatures } = await import("./license");
|
||||
mockCache.get.mockRejectedValue(new Error("Cache error"));
|
||||
|
||||
const features = await getLicenseFeatures();
|
||||
expect(features).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Cache Key Generation", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
mockCache.get.mockReset();
|
||||
mockCache.set.mockReset();
|
||||
mockCache.del.mockReset();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
test("should use 'browser' as cache key in browser environment", async () => {
|
||||
vi.stubGlobal("window", {});
|
||||
const { getEnterpriseLicense } = await import("./license");
|
||||
await getEnterpriseLicense();
|
||||
expect(mockCache.get).toHaveBeenCalledWith(
|
||||
expect.stringContaining("formbricksEnterpriseLicense-details-browser")
|
||||
);
|
||||
});
|
||||
|
||||
test("should use 'no-license' as cache key when ENTERPRISE_LICENSE_KEY is not set", async () => {
|
||||
vi.resetModules();
|
||||
vi.stubGlobal("window", undefined);
|
||||
vi.doMock("@/lib/env", () => ({
|
||||
env: {
|
||||
ENTERPRISE_LICENSE_KEY: undefined,
|
||||
VERCEL_URL: "some.vercel.url",
|
||||
FORMBRICKS_COM_URL: "https://app.formbricks.com",
|
||||
HTTPS_PROXY: undefined,
|
||||
HTTP_PROXY: undefined,
|
||||
},
|
||||
}));
|
||||
const { getEnterpriseLicense } = await import("./license");
|
||||
await getEnterpriseLicense();
|
||||
// The cache should NOT be accessed if there is no license key
|
||||
expect(mockCache.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should use hashed license key as cache key when ENTERPRISE_LICENSE_KEY is set", async () => {
|
||||
vi.resetModules();
|
||||
const testLicenseKey = "test-license-key";
|
||||
vi.stubGlobal("window", undefined);
|
||||
vi.doMock("@/lib/env", () => ({
|
||||
env: {
|
||||
ENTERPRISE_LICENSE_KEY: testLicenseKey,
|
||||
VERCEL_URL: "some.vercel.url",
|
||||
FORMBRICKS_COM_URL: "https://app.formbricks.com",
|
||||
HTTPS_PROXY: undefined,
|
||||
HTTP_PROXY: undefined,
|
||||
},
|
||||
}));
|
||||
const { hashString } = await import("@/lib/hashString");
|
||||
const expectedHash = hashString(testLicenseKey);
|
||||
const { getEnterpriseLicense } = await import("./license");
|
||||
await getEnterpriseLicense();
|
||||
expect(mockCache.get).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`formbricksEnterpriseLicense-details-${expectedHash}`)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Helper mock for process.env if not already globally available in test environment
|
||||
if (typeof process === "undefined") {
|
||||
global.process = { env: {} } as any;
|
||||
}
|
||||
vi.stubGlobal("process", global.process);
|
||||
395
apps/web/modules/ee/license-check/lib/license.ts
Normal file
395
apps/web/modules/ee/license-check/lib/license.ts
Normal file
@@ -0,0 +1,395 @@
|
||||
import { env } from "@/lib/env";
|
||||
import { hashString } from "@/lib/hashString";
|
||||
import { getCache } from "@/modules/cache/lib/service";
|
||||
import {
|
||||
TEnterpriseLicenseDetails,
|
||||
TEnterpriseLicenseFeatures,
|
||||
} from "@/modules/ee/license-check/types/enterprise-license";
|
||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||
import fetch from "node-fetch";
|
||||
import { cache as reactCache } from "react";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { logger } from "@formbricks/logger";
|
||||
|
||||
// Configuration
|
||||
const CONFIG = {
|
||||
CACHE: {
|
||||
FETCH_LICENSE_TTL_MS: 24 * 60 * 60 * 1000, // 24 hours
|
||||
PREVIOUS_RESULT_TTL_MS: 4 * 24 * 60 * 60 * 1000, // 4 days
|
||||
GRACE_PERIOD_MS: 3 * 24 * 60 * 60 * 1000, // 3 days
|
||||
MAX_RETRIES: 3,
|
||||
RETRY_DELAY_MS: 1000,
|
||||
},
|
||||
API: {
|
||||
ENDPOINT: "https://ee.formbricks.com/api/licenses/check",
|
||||
TIMEOUT_MS: 5000,
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Types
|
||||
type FallbackLevel = "live" | "cached" | "grace" | "default";
|
||||
|
||||
type TPreviousResult = {
|
||||
active: boolean;
|
||||
lastChecked: Date;
|
||||
features: TEnterpriseLicenseFeatures | null;
|
||||
version: number; // For cache versioning
|
||||
};
|
||||
|
||||
// Validation schemas
|
||||
const LicenseFeaturesSchema = z.object({
|
||||
isMultiOrgEnabled: z.boolean(),
|
||||
projects: z.number().nullable(),
|
||||
twoFactorAuth: z.boolean(),
|
||||
sso: z.boolean(),
|
||||
whitelabel: z.boolean(),
|
||||
removeBranding: z.boolean(),
|
||||
contacts: z.boolean(),
|
||||
ai: z.boolean(),
|
||||
saml: z.boolean(),
|
||||
spamProtection: z.boolean(),
|
||||
});
|
||||
|
||||
const LicenseDetailsSchema = z.object({
|
||||
status: z.enum(["active", "expired"]),
|
||||
features: LicenseFeaturesSchema,
|
||||
});
|
||||
|
||||
// Error types
|
||||
class LicenseError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly code: string
|
||||
) {
|
||||
super(message);
|
||||
this.name = "LicenseError";
|
||||
}
|
||||
}
|
||||
|
||||
class LicenseApiError extends LicenseError {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly status: number
|
||||
) {
|
||||
super(message, "API_ERROR");
|
||||
this.name = "LicenseApiError";
|
||||
}
|
||||
}
|
||||
|
||||
// Cache keys
|
||||
const getHashedKey = () => {
|
||||
if (typeof window !== "undefined") {
|
||||
return "browser"; // Browser environment
|
||||
}
|
||||
if (!env.ENTERPRISE_LICENSE_KEY) {
|
||||
return "no-license"; // No license key provided
|
||||
}
|
||||
return hashString(env.ENTERPRISE_LICENSE_KEY); // Valid license key
|
||||
};
|
||||
|
||||
export const getCacheKeys = () => {
|
||||
const hashedKey = getHashedKey();
|
||||
return {
|
||||
FETCH_LICENSE_CACHE_KEY: `formbricksEnterpriseLicense-details-${hashedKey}`,
|
||||
PREVIOUS_RESULT_CACHE_KEY: `formbricksEnterpriseLicense-previousResult-${hashedKey}`,
|
||||
};
|
||||
};
|
||||
|
||||
// Default features
|
||||
const DEFAULT_FEATURES: TEnterpriseLicenseFeatures = {
|
||||
isMultiOrgEnabled: false,
|
||||
projects: 3,
|
||||
twoFactorAuth: false,
|
||||
sso: false,
|
||||
whitelabel: false,
|
||||
removeBranding: false,
|
||||
contacts: false,
|
||||
ai: false,
|
||||
saml: false,
|
||||
spamProtection: false,
|
||||
};
|
||||
|
||||
// Helper functions
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const validateConfig = () => {
|
||||
const errors: string[] = [];
|
||||
if (CONFIG.CACHE.GRACE_PERIOD_MS >= CONFIG.CACHE.PREVIOUS_RESULT_TTL_MS) {
|
||||
errors.push("Grace period must be shorter than previous result TTL");
|
||||
}
|
||||
if (CONFIG.CACHE.MAX_RETRIES < 0) {
|
||||
errors.push("Max retries must be non-negative");
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
throw new LicenseError(errors.join(", "), "CONFIG_ERROR");
|
||||
}
|
||||
};
|
||||
|
||||
// Cache functions
|
||||
const getPreviousResult = async (): Promise<TPreviousResult> => {
|
||||
if (typeof window !== "undefined") {
|
||||
return {
|
||||
active: false,
|
||||
lastChecked: new Date(0),
|
||||
features: DEFAULT_FEATURES,
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
const formbricksCache = getCache();
|
||||
const cachedData = await formbricksCache.get<TPreviousResult>(getCacheKeys().PREVIOUS_RESULT_CACHE_KEY);
|
||||
if (cachedData) {
|
||||
return {
|
||||
...cachedData,
|
||||
lastChecked: new Date(cachedData.lastChecked),
|
||||
};
|
||||
}
|
||||
return {
|
||||
active: false,
|
||||
lastChecked: new Date(0),
|
||||
features: DEFAULT_FEATURES,
|
||||
version: 1,
|
||||
};
|
||||
};
|
||||
|
||||
const setPreviousResult = async (previousResult: TPreviousResult) => {
|
||||
if (typeof window !== "undefined") return;
|
||||
|
||||
const formbricksCache = getCache();
|
||||
await formbricksCache.set(
|
||||
getCacheKeys().PREVIOUS_RESULT_CACHE_KEY,
|
||||
previousResult,
|
||||
CONFIG.CACHE.PREVIOUS_RESULT_TTL_MS
|
||||
);
|
||||
};
|
||||
|
||||
// Monitoring functions
|
||||
const trackFallbackUsage = (level: FallbackLevel) => {
|
||||
logger.info(`Using license fallback level: ${level}`, {
|
||||
fallbackLevel: level,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
};
|
||||
|
||||
const trackApiError = (error: LicenseApiError) => {
|
||||
logger.error(`License API error: ${error.message}`, {
|
||||
status: error.status,
|
||||
code: error.code,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
};
|
||||
|
||||
// Validation functions
|
||||
const validateFallback = (previousResult: TPreviousResult): boolean => {
|
||||
if (!previousResult.features) return false;
|
||||
if (previousResult.lastChecked.getTime() === new Date(0).getTime()) return false;
|
||||
if (previousResult.version !== 1) return false; // Add version check
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateLicenseDetails = (data: unknown): TEnterpriseLicenseDetails => {
|
||||
return LicenseDetailsSchema.parse(data);
|
||||
};
|
||||
|
||||
// Fallback functions
|
||||
const getFallbackLevel = (
|
||||
liveLicense: TEnterpriseLicenseDetails | null,
|
||||
previousResult: TPreviousResult,
|
||||
currentTime: Date
|
||||
): FallbackLevel => {
|
||||
if (liveLicense) return "live";
|
||||
if (previousResult.active) {
|
||||
const elapsedTime = currentTime.getTime() - previousResult.lastChecked.getTime();
|
||||
return elapsedTime < CONFIG.CACHE.GRACE_PERIOD_MS ? "grace" : "default";
|
||||
}
|
||||
return "default";
|
||||
};
|
||||
|
||||
const handleInitialFailure = async (currentTime: Date) => {
|
||||
const initialFailResult: TPreviousResult = {
|
||||
active: false,
|
||||
features: DEFAULT_FEATURES,
|
||||
lastChecked: currentTime,
|
||||
version: 1,
|
||||
};
|
||||
await setPreviousResult(initialFailResult);
|
||||
return {
|
||||
active: false,
|
||||
features: DEFAULT_FEATURES,
|
||||
lastChecked: currentTime,
|
||||
isPendingDowngrade: false,
|
||||
fallbackLevel: "default" as const,
|
||||
};
|
||||
};
|
||||
|
||||
// API functions
|
||||
const fetchLicenseFromServerInternal = async (retryCount = 0): Promise<TEnterpriseLicenseDetails | null> => {
|
||||
if (!env.ENTERPRISE_LICENSE_KEY) return null;
|
||||
|
||||
// Skip license checks during build time
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars -- NEXT_PHASE is a next.js env variable
|
||||
if (process.env.NEXT_PHASE === "phase-production-build") {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const now = new Date();
|
||||
const startOfYear = new Date(now.getFullYear(), 0, 1);
|
||||
// first millisecond of next year => current year is fully included
|
||||
const startOfNextYear = new Date(now.getFullYear() + 1, 0, 1);
|
||||
|
||||
const responseCount = await prisma.response.count({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: startOfYear,
|
||||
lt: startOfNextYear,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const proxyUrl = env.HTTPS_PROXY ?? env.HTTP_PROXY;
|
||||
const agent = proxyUrl ? new HttpsProxyAgent(proxyUrl) : undefined;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), CONFIG.API.TIMEOUT_MS);
|
||||
|
||||
const res = await fetch(CONFIG.API.ENDPOINT, {
|
||||
body: JSON.stringify({
|
||||
licenseKey: env.ENTERPRISE_LICENSE_KEY,
|
||||
usage: { responseCount },
|
||||
}),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
agent,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (res.ok) {
|
||||
const responseJson = (await res.json()) as { data: unknown };
|
||||
return validateLicenseDetails(responseJson.data);
|
||||
}
|
||||
|
||||
const error = new LicenseApiError(`License check API responded with status: ${res.status}`, res.status);
|
||||
trackApiError(error);
|
||||
|
||||
// Retry on specific status codes
|
||||
if (retryCount < CONFIG.CACHE.MAX_RETRIES && [429, 502, 503, 504].includes(res.status)) {
|
||||
await sleep(CONFIG.CACHE.RETRY_DELAY_MS * Math.pow(2, retryCount));
|
||||
return fetchLicenseFromServerInternal(retryCount + 1);
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
if (error instanceof LicenseApiError) {
|
||||
throw error;
|
||||
}
|
||||
logger.error(error, "Error while fetching license from server");
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchLicense = async (): Promise<TEnterpriseLicenseDetails | null> => {
|
||||
if (!env.ENTERPRISE_LICENSE_KEY) return null;
|
||||
|
||||
const formbricksCache = getCache();
|
||||
const cachedLicense = await formbricksCache.get<TEnterpriseLicenseDetails>(
|
||||
getCacheKeys().FETCH_LICENSE_CACHE_KEY
|
||||
);
|
||||
|
||||
if (cachedLicense) {
|
||||
return cachedLicense;
|
||||
}
|
||||
|
||||
const licenseDetails = await fetchLicenseFromServerInternal();
|
||||
|
||||
if (licenseDetails) {
|
||||
await formbricksCache.set(
|
||||
getCacheKeys().FETCH_LICENSE_CACHE_KEY,
|
||||
licenseDetails,
|
||||
CONFIG.CACHE.FETCH_LICENSE_TTL_MS
|
||||
);
|
||||
}
|
||||
return licenseDetails;
|
||||
};
|
||||
|
||||
export const getEnterpriseLicense = reactCache(
|
||||
async (): Promise<{
|
||||
active: boolean;
|
||||
features: TEnterpriseLicenseFeatures | null;
|
||||
lastChecked: Date;
|
||||
isPendingDowngrade: boolean;
|
||||
fallbackLevel: FallbackLevel;
|
||||
}> => {
|
||||
validateConfig();
|
||||
|
||||
if (!env.ENTERPRISE_LICENSE_KEY || env.ENTERPRISE_LICENSE_KEY.length === 0) {
|
||||
return {
|
||||
active: false,
|
||||
features: null,
|
||||
lastChecked: new Date(),
|
||||
isPendingDowngrade: false,
|
||||
fallbackLevel: "default" as const,
|
||||
};
|
||||
}
|
||||
|
||||
const currentTime = new Date();
|
||||
const liveLicenseDetails = await fetchLicense();
|
||||
const previousResult = await getPreviousResult();
|
||||
const fallbackLevel = getFallbackLevel(liveLicenseDetails, previousResult, currentTime);
|
||||
|
||||
trackFallbackUsage(fallbackLevel);
|
||||
|
||||
let currentLicenseState: TPreviousResult | undefined;
|
||||
|
||||
switch (fallbackLevel) {
|
||||
case "live":
|
||||
if (!liveLicenseDetails) throw new Error("Invalid state: live license expected");
|
||||
currentLicenseState = {
|
||||
active: liveLicenseDetails.status === "active",
|
||||
features: liveLicenseDetails.features,
|
||||
lastChecked: currentTime,
|
||||
version: 1,
|
||||
};
|
||||
await setPreviousResult(currentLicenseState);
|
||||
return {
|
||||
active: currentLicenseState.active,
|
||||
features: currentLicenseState.features,
|
||||
lastChecked: currentTime,
|
||||
isPendingDowngrade: false,
|
||||
fallbackLevel: "live" as const,
|
||||
};
|
||||
|
||||
case "grace":
|
||||
if (!validateFallback(previousResult)) {
|
||||
return handleInitialFailure(currentTime);
|
||||
}
|
||||
return {
|
||||
active: previousResult.active,
|
||||
features: previousResult.features,
|
||||
lastChecked: previousResult.lastChecked,
|
||||
isPendingDowngrade: true,
|
||||
fallbackLevel: "grace" as const,
|
||||
};
|
||||
|
||||
case "default":
|
||||
return handleInitialFailure(currentTime);
|
||||
}
|
||||
|
||||
return handleInitialFailure(currentTime);
|
||||
}
|
||||
);
|
||||
|
||||
export const getLicenseFeatures = async (): Promise<TEnterpriseLicenseFeatures | null> => {
|
||||
try {
|
||||
const licenseState = await getEnterpriseLicense();
|
||||
return licenseState.active ? licenseState.features : null;
|
||||
} catch (e) {
|
||||
logger.error(e, "Error getting license features");
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// All permission checking functions and their helpers have been moved to utils.ts
|
||||
@@ -1,140 +1,479 @@
|
||||
import * as constants from "@/lib/constants";
|
||||
import { TEnterpriseLicenseFeatures } from "@/modules/ee/license-check/types/enterprise-license";
|
||||
import { Organization } from "@prisma/client";
|
||||
import fetch from "node-fetch";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import * as licenseModule from "./license";
|
||||
import {
|
||||
getBiggerUploadFileSizePermission,
|
||||
getIsContactsEnabled,
|
||||
getIsMultiOrgEnabled,
|
||||
getIsSamlSsoEnabled,
|
||||
getIsSpamProtectionEnabled,
|
||||
getIsSsoEnabled,
|
||||
getIsTwoFactorAuthEnabled,
|
||||
getLicenseFeatures,
|
||||
getMultiLanguagePermission,
|
||||
getOrganizationProjectsLimit,
|
||||
getRemoveBrandingPermission,
|
||||
getRoleManagementPermission,
|
||||
getWhiteLabelPermission,
|
||||
getisSsoEnabled,
|
||||
} from "./utils";
|
||||
|
||||
// Mock declarations must be at the top level
|
||||
vi.mock("@/lib/env", () => ({
|
||||
env: {
|
||||
ENTERPRISE_LICENSE_KEY: "test-license-key",
|
||||
},
|
||||
}));
|
||||
vi.mock("@/lib/constants");
|
||||
vi.mock("./license");
|
||||
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
E2E_TESTING: false,
|
||||
ENTERPRISE_LICENSE_KEY: "test-license-key",
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
IS_RECAPTCHA_CONFIGURED: true,
|
||||
PROJECT_FEATURE_KEYS: {
|
||||
removeBranding: "remove-branding",
|
||||
whiteLabel: "white-label",
|
||||
roleManagement: "role-management",
|
||||
biggerUploadFileSize: "bigger-upload-file-size",
|
||||
multiLanguage: "multi-language",
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/cache", () => ({
|
||||
cache: vi.fn((fn) => fn),
|
||||
revalidateTag: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("next/server", () => ({
|
||||
after: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("node-fetch", () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("License Check Utils", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Feature Permissions", () => {
|
||||
const mockOrganization = {
|
||||
billing: {
|
||||
plan: "enterprise" as Organization["billing"]["plan"],
|
||||
limits: {
|
||||
projects: 3,
|
||||
},
|
||||
const mockOrganization = {
|
||||
billing: {
|
||||
plan: constants.PROJECT_FEATURE_KEYS.FREE,
|
||||
limits: {
|
||||
projects: 3,
|
||||
monthly: {
|
||||
responses: null,
|
||||
miu: null,
|
||||
},
|
||||
} as Organization;
|
||||
},
|
||||
},
|
||||
} as Organization;
|
||||
|
||||
test("getRemoveBrandingPermission", async () => {
|
||||
const defaultFeatures: TEnterpriseLicenseFeatures = {
|
||||
whitelabel: false,
|
||||
projects: null,
|
||||
isMultiOrgEnabled: false,
|
||||
contacts: false,
|
||||
removeBranding: false,
|
||||
twoFactorAuth: false,
|
||||
sso: false,
|
||||
saml: false,
|
||||
spamProtection: false,
|
||||
ai: false,
|
||||
};
|
||||
|
||||
const defaultLicense = {
|
||||
active: true,
|
||||
features: defaultFeatures,
|
||||
lastChecked: new Date(),
|
||||
isPendingDowngrade: false,
|
||||
fallbackLevel: "live" as const,
|
||||
};
|
||||
|
||||
describe("License Utils", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
// Set default values for constants
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = false;
|
||||
vi.mocked(constants).IS_RECAPTCHA_CONFIGURED = true;
|
||||
vi.mocked(constants).PROJECT_FEATURE_KEYS = constants.PROJECT_FEATURE_KEYS;
|
||||
// Set default mocks for license
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
|
||||
vi.mocked(licenseModule.getLicenseFeatures).mockResolvedValue(defaultFeatures);
|
||||
});
|
||||
|
||||
describe("getRemoveBrandingPermission", () => {
|
||||
test("should return true if license active and feature enabled (self-hosted)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = false;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
features: { ...defaultFeatures, removeBranding: true },
|
||||
});
|
||||
const result = await getRemoveBrandingPermission(mockOrganization.billing.plan);
|
||||
expect(result).toBe(false); // Default value when no license is active
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("getWhiteLabelPermission", async () => {
|
||||
const result = await getWhiteLabelPermission(mockOrganization.billing.plan);
|
||||
expect(result).toBe(false); // Default value when no license is active
|
||||
test("should return false if license active but feature disabled (self-hosted)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = false;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
features: { ...defaultFeatures, removeBranding: false },
|
||||
});
|
||||
const result = await getRemoveBrandingPermission(mockOrganization.billing.plan);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("getRoleManagementPermission", async () => {
|
||||
const result = await getRoleManagementPermission(mockOrganization.billing.plan);
|
||||
expect(result).toBe(false); // Default value when no license is active
|
||||
test("should return true if license active and plan is not FREE (cloud)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
|
||||
const result = await getRemoveBrandingPermission(constants.PROJECT_FEATURE_KEYS.SCALE);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("getBiggerUploadFileSizePermission", async () => {
|
||||
const result = await getBiggerUploadFileSizePermission(mockOrganization.billing.plan);
|
||||
expect(result).toBe(false); // Default value when no license is active
|
||||
test("should return false if license active and plan is FREE (cloud)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
|
||||
const result = await getRemoveBrandingPermission(constants.PROJECT_FEATURE_KEYS.FREE);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("getMultiLanguagePermission", async () => {
|
||||
const result = await getMultiLanguagePermission(mockOrganization.billing.plan);
|
||||
expect(result).toBe(false); // Default value when no license is active
|
||||
});
|
||||
|
||||
test("getIsMultiOrgEnabled", async () => {
|
||||
const result = await getIsMultiOrgEnabled();
|
||||
expect(typeof result).toBe("boolean");
|
||||
});
|
||||
|
||||
test("getIsContactsEnabled", async () => {
|
||||
const result = await getIsContactsEnabled();
|
||||
expect(typeof result).toBe("boolean");
|
||||
});
|
||||
|
||||
test("getIsTwoFactorAuthEnabled", async () => {
|
||||
const result = await getIsTwoFactorAuthEnabled();
|
||||
expect(typeof result).toBe("boolean");
|
||||
});
|
||||
|
||||
test("getisSsoEnabled", async () => {
|
||||
const result = await getisSsoEnabled();
|
||||
expect(typeof result).toBe("boolean");
|
||||
});
|
||||
|
||||
test("getIsSamlSsoEnabled", async () => {
|
||||
const result = await getIsSamlSsoEnabled();
|
||||
expect(typeof result).toBe("boolean");
|
||||
});
|
||||
|
||||
test("getIsSpamProtectionEnabled", async () => {
|
||||
const result = await getIsSpamProtectionEnabled(mockOrganization.billing.plan);
|
||||
expect(result).toBe(false); // Default value when no license is active
|
||||
});
|
||||
|
||||
test("getOrganizationProjectsLimit", async () => {
|
||||
const result = await getOrganizationProjectsLimit(mockOrganization.billing.limits);
|
||||
expect(result).toBe(3); // Default value from mock organization
|
||||
test("should return false if license is inactive", async () => {
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
active: false,
|
||||
});
|
||||
const result = await getRemoveBrandingPermission(mockOrganization.billing.plan);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("License Features", () => {
|
||||
test("getLicenseFeatures returns null when no license is active", async () => {
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
} as any);
|
||||
describe("getWhiteLabelPermission", () => {
|
||||
test("should return true if license active and feature enabled (self-hosted)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = false;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
features: { ...defaultFeatures, whitelabel: true },
|
||||
});
|
||||
const result = await getWhiteLabelPermission(mockOrganization.billing.plan);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
const result = await getLicenseFeatures();
|
||||
expect(result).toBeNull();
|
||||
test("should return true if license active and plan is not FREE (cloud)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
|
||||
const result = await getWhiteLabelPermission(constants.PROJECT_FEATURE_KEYS.SCALE);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false if license is inactive", async () => {
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
active: false,
|
||||
});
|
||||
const result = await getWhiteLabelPermission(mockOrganization.billing.plan);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRoleManagementPermission", () => {
|
||||
test("should return true if license active (self-hosted)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = false;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
|
||||
const result = await getRoleManagementPermission(mockOrganization.billing.plan);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return true if license active and plan is SCALE (cloud)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
|
||||
const result = await getRoleManagementPermission(constants.PROJECT_FEATURE_KEYS.SCALE);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return true if license active and plan is ENTERPRISE (cloud)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
|
||||
const result = await getRoleManagementPermission(constants.PROJECT_FEATURE_KEYS.ENTERPRISE);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false if license active and plan is not SCALE or ENTERPRISE (cloud)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
|
||||
const result = await getRoleManagementPermission(constants.PROJECT_FEATURE_KEYS.STARTUP);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false if license is inactive", async () => {
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
active: false,
|
||||
});
|
||||
const result = await getRoleManagementPermission(mockOrganization.billing.plan);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBiggerUploadFileSizePermission", () => {
|
||||
test("should return true if license active (self-hosted)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = false;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
|
||||
const result = await getBiggerUploadFileSizePermission(mockOrganization.billing.plan);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return true if license active and plan is not FREE (cloud)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
|
||||
const result = await getBiggerUploadFileSizePermission(constants.PROJECT_FEATURE_KEYS.SCALE);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false if license active and plan is FREE (cloud)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
|
||||
const result = await getBiggerUploadFileSizePermission(constants.PROJECT_FEATURE_KEYS.FREE);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false if license is inactive", async () => {
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
active: false,
|
||||
});
|
||||
const result = await getBiggerUploadFileSizePermission(mockOrganization.billing.plan);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMultiLanguagePermission", () => {
|
||||
test("should return true if license active (self-hosted)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = false;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
|
||||
const result = await getMultiLanguagePermission(mockOrganization.billing.plan);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return true if license active and plan is SCALE (cloud)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
|
||||
const result = await getMultiLanguagePermission(constants.PROJECT_FEATURE_KEYS.SCALE);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false if license is inactive", async () => {
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
active: false,
|
||||
});
|
||||
const result = await getMultiLanguagePermission(mockOrganization.billing.plan);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getIsMultiOrgEnabled", () => {
|
||||
test("should return true if feature flag isMultiOrgEnabled is true", async () => {
|
||||
vi.mocked(licenseModule.getLicenseFeatures).mockResolvedValue({
|
||||
...defaultFeatures,
|
||||
isMultiOrgEnabled: true,
|
||||
});
|
||||
const result = await getIsMultiOrgEnabled();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false if feature flag isMultiOrgEnabled is false", async () => {
|
||||
vi.mocked(licenseModule.getLicenseFeatures).mockResolvedValue({
|
||||
...defaultFeatures,
|
||||
isMultiOrgEnabled: false,
|
||||
});
|
||||
const result = await getIsMultiOrgEnabled();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false if licenseFeatures is null", async () => {
|
||||
vi.mocked(licenseModule.getLicenseFeatures).mockResolvedValue(null);
|
||||
const result = await getIsMultiOrgEnabled();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getIsContactsEnabled", () => {
|
||||
test("should return true if feature flag contacts is true", async () => {
|
||||
vi.mocked(licenseModule.getLicenseFeatures).mockResolvedValue({
|
||||
...defaultFeatures,
|
||||
contacts: true,
|
||||
});
|
||||
const result = await getIsContactsEnabled();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false if feature flag contacts is false", async () => {
|
||||
vi.mocked(licenseModule.getLicenseFeatures).mockResolvedValue({
|
||||
...defaultFeatures,
|
||||
contacts: false,
|
||||
});
|
||||
const result = await getIsContactsEnabled();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getIsTwoFactorAuthEnabled", () => {
|
||||
test("should return true if feature flag twoFactorAuth is true", async () => {
|
||||
vi.mocked(licenseModule.getLicenseFeatures).mockResolvedValue({
|
||||
...defaultFeatures,
|
||||
twoFactorAuth: true,
|
||||
});
|
||||
const result = await getIsTwoFactorAuthEnabled();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false if feature flag twoFactorAuth is false", async () => {
|
||||
vi.mocked(licenseModule.getLicenseFeatures).mockResolvedValue({
|
||||
...defaultFeatures,
|
||||
twoFactorAuth: false,
|
||||
});
|
||||
const result = await getIsTwoFactorAuthEnabled();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getIsSsoEnabled", () => {
|
||||
test("should return true if feature flag sso is true", async () => {
|
||||
vi.mocked(licenseModule.getLicenseFeatures).mockResolvedValue({
|
||||
...defaultFeatures,
|
||||
sso: true,
|
||||
});
|
||||
const result = await getIsSsoEnabled();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false if feature flag sso is false", async () => {
|
||||
vi.mocked(licenseModule.getLicenseFeatures).mockResolvedValue({
|
||||
...defaultFeatures,
|
||||
sso: false,
|
||||
});
|
||||
const result = await getIsSsoEnabled();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getIsSamlSsoEnabled", () => {
|
||||
test("should return false if IS_FORMBRICKS_CLOUD is true", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
|
||||
const result = await getIsSamlSsoEnabled();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("should return true if sso and saml flags are true (self-hosted)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = false;
|
||||
vi.mocked(licenseModule.getLicenseFeatures).mockResolvedValue({
|
||||
...defaultFeatures,
|
||||
sso: true,
|
||||
saml: true,
|
||||
});
|
||||
const result = await getIsSamlSsoEnabled();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false if sso is true but saml is false (self-hosted)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = false;
|
||||
vi.mocked(licenseModule.getLicenseFeatures).mockResolvedValue({
|
||||
...defaultFeatures,
|
||||
sso: true,
|
||||
saml: false,
|
||||
});
|
||||
const result = await getIsSamlSsoEnabled();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false if licenseFeatures is null (self-hosted)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = false;
|
||||
vi.mocked(licenseModule.getLicenseFeatures).mockResolvedValue(null);
|
||||
const result = await getIsSamlSsoEnabled();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getIsSpamProtectionEnabled", () => {
|
||||
test("should return false if IS_RECAPTCHA_CONFIGURED is false", async () => {
|
||||
vi.mocked(constants).IS_RECAPTCHA_CONFIGURED = false;
|
||||
const result = await getIsSpamProtectionEnabled(mockOrganization.billing.plan);
|
||||
expect(result).toBe(false);
|
||||
vi.mocked(constants).IS_RECAPTCHA_CONFIGURED = true; // reset for other tests
|
||||
});
|
||||
|
||||
test("should return true if license active, feature enabled, and plan is SCALE (cloud)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
features: { ...defaultFeatures, spamProtection: true },
|
||||
});
|
||||
const result = await getIsSpamProtectionEnabled(constants.PROJECT_FEATURE_KEYS.SCALE);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false if license active, feature enabled, but plan is not SCALE or ENTERPRISE (cloud)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
features: { ...defaultFeatures, spamProtection: true },
|
||||
});
|
||||
const result = await getIsSpamProtectionEnabled(constants.PROJECT_FEATURE_KEYS.STARTUP);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("should return true if license active and feature enabled (self-hosted)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = false;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
features: { ...defaultFeatures, spamProtection: true },
|
||||
});
|
||||
const result = await getIsSpamProtectionEnabled(mockOrganization.billing.plan);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false if license is inactive", async () => {
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
active: false,
|
||||
});
|
||||
const result = await getIsSpamProtectionEnabled(mockOrganization.billing.plan);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOrganizationProjectsLimit", () => {
|
||||
test("should return limits.projects if license active (cloud)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
|
||||
const limits = {
|
||||
projects: 10,
|
||||
monthly: {
|
||||
responses: null,
|
||||
miu: null,
|
||||
},
|
||||
};
|
||||
const result = await getOrganizationProjectsLimit(limits);
|
||||
expect(result).toBe(10);
|
||||
});
|
||||
|
||||
test("should return Infinity if limits.projects is null and license active (cloud)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue(defaultLicense);
|
||||
const limits = {
|
||||
projects: null,
|
||||
monthly: {
|
||||
responses: null,
|
||||
miu: null,
|
||||
},
|
||||
};
|
||||
const result = await getOrganizationProjectsLimit(limits);
|
||||
expect(result).toBe(Infinity);
|
||||
});
|
||||
|
||||
test("should return 3 if license inactive (cloud)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = true;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
active: false,
|
||||
});
|
||||
const result = await getOrganizationProjectsLimit(mockOrganization.billing.limits);
|
||||
expect(result).toBe(3);
|
||||
});
|
||||
|
||||
test("should return license.features.projects if defined and license active (self-hosted)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = false;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
features: { ...defaultFeatures, projects: 5 },
|
||||
});
|
||||
const result = await getOrganizationProjectsLimit(mockOrganization.billing.limits);
|
||||
expect(result).toBe(5);
|
||||
});
|
||||
|
||||
test("should return 3 if license.features.projects is undefined and license active (self-hosted)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = false;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
features: { ...defaultFeatures, projects: null },
|
||||
});
|
||||
const result = await getOrganizationProjectsLimit(mockOrganization.billing.limits);
|
||||
expect(result).toBe(3);
|
||||
});
|
||||
|
||||
test("should return 3 if license inactive (self-hosted)", async () => {
|
||||
vi.mocked(constants).IS_FORMBRICKS_CLOUD = false;
|
||||
vi.mocked(licenseModule.getEnterpriseLicense).mockResolvedValue({
|
||||
...defaultLicense,
|
||||
active: false,
|
||||
});
|
||||
const result = await getOrganizationProjectsLimit(mockOrganization.billing.limits);
|
||||
expect(result).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,388 +1,102 @@
|
||||
import "server-only";
|
||||
import { cache, revalidateTag } from "@/lib/cache";
|
||||
import {
|
||||
E2E_TESTING,
|
||||
ENTERPRISE_LICENSE_KEY,
|
||||
IS_FORMBRICKS_CLOUD,
|
||||
IS_RECAPTCHA_CONFIGURED,
|
||||
PROJECT_FEATURE_KEYS,
|
||||
} from "@/lib/constants";
|
||||
import { env } from "@/lib/env";
|
||||
import { hashString } from "@/lib/hashString";
|
||||
import {
|
||||
TEnterpriseLicenseDetails,
|
||||
TEnterpriseLicenseFeatures,
|
||||
} from "@/modules/ee/license-check/types/enterprise-license";
|
||||
import { IS_FORMBRICKS_CLOUD, IS_RECAPTCHA_CONFIGURED, PROJECT_FEATURE_KEYS } from "@/lib/constants";
|
||||
import { TEnterpriseLicenseFeatures } from "@/modules/ee/license-check/types/enterprise-license";
|
||||
import { Organization } from "@prisma/client";
|
||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||
import { after } from "next/server";
|
||||
import fetch from "node-fetch";
|
||||
import { cache as reactCache } from "react";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { getEnterpriseLicense, getLicenseFeatures } from "./license";
|
||||
|
||||
const hashedKey = ENTERPRISE_LICENSE_KEY ? hashString(ENTERPRISE_LICENSE_KEY) : undefined;
|
||||
const PREVIOUS_RESULTS_CACHE_TAG_KEY = `getPreviousResult-${hashedKey}` as const;
|
||||
// Helper function for feature permissions (e.g., removeBranding, whitelabel)
|
||||
const getFeaturePermission = async (
|
||||
billingPlan: Organization["billing"]["plan"],
|
||||
featureKey: keyof Pick<TEnterpriseLicenseFeatures, "removeBranding" | "whitelabel">
|
||||
): Promise<boolean> => {
|
||||
const license = await getEnterpriseLicense();
|
||||
|
||||
// This function is used to get the previous result of the license check from the cache
|
||||
// This might seem confusing at first since we only return the default value from this function,
|
||||
// but since we are using a cache and the cache key is the same, the cache will return the previous result - so this function acts as a cache getter
|
||||
const getPreviousResult = (): Promise<{
|
||||
active: boolean | null;
|
||||
lastChecked: Date;
|
||||
features: TEnterpriseLicenseFeatures | null;
|
||||
}> =>
|
||||
cache(
|
||||
async () => ({
|
||||
active: null,
|
||||
lastChecked: new Date(0),
|
||||
features: null,
|
||||
}),
|
||||
[PREVIOUS_RESULTS_CACHE_TAG_KEY],
|
||||
{
|
||||
tags: [PREVIOUS_RESULTS_CACHE_TAG_KEY],
|
||||
}
|
||||
)();
|
||||
|
||||
// This function is used to set the previous result of the license check to the cache so that we can use it in the next call
|
||||
// Uses the same cache key as the getPreviousResult function
|
||||
const setPreviousResult = async (previousResult: {
|
||||
active: boolean | null;
|
||||
lastChecked: Date;
|
||||
features: TEnterpriseLicenseFeatures | null;
|
||||
}) => {
|
||||
const { lastChecked, active, features } = previousResult;
|
||||
|
||||
await cache(
|
||||
async () => ({
|
||||
active,
|
||||
lastChecked,
|
||||
features,
|
||||
}),
|
||||
[PREVIOUS_RESULTS_CACHE_TAG_KEY],
|
||||
{
|
||||
tags: [PREVIOUS_RESULTS_CACHE_TAG_KEY],
|
||||
}
|
||||
)();
|
||||
|
||||
after(() => {
|
||||
revalidateTag(PREVIOUS_RESULTS_CACHE_TAG_KEY);
|
||||
});
|
||||
};
|
||||
|
||||
const fetchLicenseForE2ETesting = async (): Promise<{
|
||||
active: boolean | null;
|
||||
lastChecked: Date;
|
||||
features: TEnterpriseLicenseFeatures | null;
|
||||
} | null> => {
|
||||
const currentTime = new Date();
|
||||
try {
|
||||
const previousResult = await getPreviousResult();
|
||||
if (previousResult.lastChecked.getTime() === new Date(0).getTime()) {
|
||||
// first call
|
||||
const newResult = {
|
||||
active: true,
|
||||
features: {
|
||||
isMultiOrgEnabled: true,
|
||||
twoFactorAuth: true,
|
||||
sso: true,
|
||||
contacts: true,
|
||||
projects: 3,
|
||||
whitelabel: true,
|
||||
removeBranding: true,
|
||||
spamProtection: true,
|
||||
ai: true,
|
||||
saml: true,
|
||||
},
|
||||
lastChecked: currentTime,
|
||||
};
|
||||
await setPreviousResult(newResult);
|
||||
return newResult;
|
||||
} else if (currentTime.getTime() - previousResult.lastChecked.getTime() > 60 * 60 * 1000) {
|
||||
// Fail after 1 hour
|
||||
logger.info("E2E_TESTING is enabled. Enterprise license was revoked after 1 hour.");
|
||||
return null;
|
||||
}
|
||||
return previousResult;
|
||||
} catch (error) {
|
||||
logger.error(error, "Error fetching license");
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const getEnterpriseLicense = async (): Promise<{
|
||||
active: boolean;
|
||||
features: TEnterpriseLicenseFeatures | null;
|
||||
lastChecked: Date;
|
||||
isPendingDowngrade?: boolean;
|
||||
}> => {
|
||||
if (!ENTERPRISE_LICENSE_KEY || ENTERPRISE_LICENSE_KEY.length === 0) {
|
||||
return {
|
||||
active: false,
|
||||
features: null,
|
||||
lastChecked: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
if (E2E_TESTING) {
|
||||
const previousResult = await fetchLicenseForE2ETesting();
|
||||
|
||||
return {
|
||||
active: previousResult?.active ?? false,
|
||||
features: previousResult ? previousResult.features : null,
|
||||
lastChecked: previousResult ? previousResult.lastChecked : new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
// if the server responds with a boolean, we return it
|
||||
// if the server errors, we return null
|
||||
// null signifies an error
|
||||
const license = await fetchLicense();
|
||||
|
||||
const isValid = license ? license.status === "active" : null;
|
||||
const threeDaysInMillis = 3 * 24 * 60 * 60 * 1000;
|
||||
const currentTime = new Date();
|
||||
|
||||
const previousResult = await getPreviousResult();
|
||||
|
||||
// Case: First time checking license and the server errors out
|
||||
if (previousResult.active === null) {
|
||||
if (isValid === null) {
|
||||
const newResult = {
|
||||
active: false,
|
||||
features: {
|
||||
isMultiOrgEnabled: false,
|
||||
projects: 3,
|
||||
twoFactorAuth: false,
|
||||
sso: false,
|
||||
whitelabel: false,
|
||||
removeBranding: false,
|
||||
contacts: false,
|
||||
ai: false,
|
||||
saml: false,
|
||||
spamProtection: false,
|
||||
},
|
||||
lastChecked: new Date(),
|
||||
};
|
||||
|
||||
await setPreviousResult(newResult);
|
||||
return newResult;
|
||||
}
|
||||
}
|
||||
|
||||
if (isValid !== null && license) {
|
||||
const newResult = {
|
||||
active: isValid,
|
||||
features: license.features,
|
||||
lastChecked: new Date(),
|
||||
};
|
||||
|
||||
await setPreviousResult(newResult);
|
||||
return newResult;
|
||||
if (IS_FORMBRICKS_CLOUD) {
|
||||
return license.active && billingPlan !== PROJECT_FEATURE_KEYS.FREE;
|
||||
} else {
|
||||
// if result is undefined -> error
|
||||
// if the last check was less than 72 hours, return the previous value:
|
||||
|
||||
const elapsedTime = currentTime.getTime() - previousResult.lastChecked.getTime();
|
||||
if (elapsedTime < threeDaysInMillis) {
|
||||
return {
|
||||
active: previousResult.active !== null ? previousResult.active : false,
|
||||
features: previousResult.features,
|
||||
lastChecked: previousResult.lastChecked,
|
||||
isPendingDowngrade: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Log error only after 72 hours
|
||||
logger.error("Error while checking license: The license check failed");
|
||||
|
||||
return {
|
||||
active: false,
|
||||
features: null,
|
||||
lastChecked: previousResult.lastChecked,
|
||||
isPendingDowngrade: true,
|
||||
};
|
||||
return license.active && !!license.features?.[featureKey];
|
||||
}
|
||||
};
|
||||
|
||||
export const getLicenseFeatures = async (): Promise<TEnterpriseLicenseFeatures | null> => {
|
||||
const previousResult = await getPreviousResult();
|
||||
if (previousResult.features) {
|
||||
return previousResult.features;
|
||||
} else {
|
||||
const license = await fetchLicense();
|
||||
if (!license || !license.features) return null;
|
||||
return license.features;
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchLicense = reactCache(
|
||||
async (): Promise<TEnterpriseLicenseDetails | null> =>
|
||||
cache(
|
||||
async () => {
|
||||
if (!env.ENTERPRISE_LICENSE_KEY) return null;
|
||||
try {
|
||||
const now = new Date();
|
||||
const startOfYear = new Date(now.getFullYear(), 0, 1); // January 1st of the current year
|
||||
const endOfYear = new Date(now.getFullYear() + 1, 0, 0); // December 31st of the current year
|
||||
|
||||
const responseCount = await prisma.response.count({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: startOfYear,
|
||||
lt: endOfYear,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const proxyUrl = env.HTTPS_PROXY || env.HTTP_PROXY;
|
||||
const agent = proxyUrl ? new HttpsProxyAgent(proxyUrl) : undefined;
|
||||
|
||||
const res = await fetch("https://ee.formbricks.com/api/licenses/check", {
|
||||
body: JSON.stringify({
|
||||
licenseKey: ENTERPRISE_LICENSE_KEY,
|
||||
usage: { responseCount: responseCount },
|
||||
}),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
agent,
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const responseJson = (await res.json()) as {
|
||||
data: TEnterpriseLicenseDetails;
|
||||
};
|
||||
return responseJson.data;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
logger.error(error, "Error while checking license");
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[`fetchLicense-${hashedKey}`],
|
||||
{ revalidate: 60 * 60 * 24 }
|
||||
)()
|
||||
);
|
||||
|
||||
export const getRemoveBrandingPermission = async (
|
||||
billingPlan: Organization["billing"]["plan"]
|
||||
): Promise<boolean> => {
|
||||
if (E2E_TESTING) {
|
||||
const previousResult = await fetchLicenseForE2ETesting();
|
||||
return previousResult?.features?.removeBranding ?? false;
|
||||
}
|
||||
|
||||
if (IS_FORMBRICKS_CLOUD && (await getEnterpriseLicense()).active) {
|
||||
return billingPlan !== PROJECT_FEATURE_KEYS.FREE;
|
||||
} else {
|
||||
const licenseFeatures = await getLicenseFeatures();
|
||||
if (!licenseFeatures) return false;
|
||||
|
||||
return licenseFeatures.removeBranding;
|
||||
}
|
||||
return getFeaturePermission(billingPlan, "removeBranding");
|
||||
};
|
||||
|
||||
export const getWhiteLabelPermission = async (
|
||||
billingPlan: Organization["billing"]["plan"]
|
||||
): Promise<boolean> => {
|
||||
if (E2E_TESTING) {
|
||||
const previousResult = await fetchLicenseForE2ETesting();
|
||||
return previousResult?.features?.whitelabel ?? false;
|
||||
}
|
||||
|
||||
if (IS_FORMBRICKS_CLOUD && (await getEnterpriseLicense()).active) {
|
||||
return billingPlan !== PROJECT_FEATURE_KEYS.FREE;
|
||||
} else {
|
||||
const licenseFeatures = await getLicenseFeatures();
|
||||
if (!licenseFeatures) return false;
|
||||
|
||||
return licenseFeatures.whitelabel;
|
||||
}
|
||||
return getFeaturePermission(billingPlan, "whitelabel");
|
||||
};
|
||||
|
||||
export const getRoleManagementPermission = async (
|
||||
billingPlan: Organization["billing"]["plan"]
|
||||
): Promise<boolean> => {
|
||||
if (E2E_TESTING) {
|
||||
const previousResult = await fetchLicenseForE2ETesting();
|
||||
return previousResult && previousResult.active !== null ? previousResult.active : false;
|
||||
}
|
||||
const license = await getEnterpriseLicense();
|
||||
|
||||
if (IS_FORMBRICKS_CLOUD)
|
||||
return billingPlan === PROJECT_FEATURE_KEYS.SCALE || billingPlan === PROJECT_FEATURE_KEYS.ENTERPRISE;
|
||||
else if (!IS_FORMBRICKS_CLOUD) return (await getEnterpriseLicense()).active;
|
||||
return (
|
||||
license.active &&
|
||||
(billingPlan === PROJECT_FEATURE_KEYS.SCALE || billingPlan === PROJECT_FEATURE_KEYS.ENTERPRISE)
|
||||
);
|
||||
else if (!IS_FORMBRICKS_CLOUD) return license.active;
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getBiggerUploadFileSizePermission = async (
|
||||
billingPlan: Organization["billing"]["plan"]
|
||||
): Promise<boolean> => {
|
||||
if (IS_FORMBRICKS_CLOUD) return billingPlan !== PROJECT_FEATURE_KEYS.FREE;
|
||||
else if (!IS_FORMBRICKS_CLOUD) return (await getEnterpriseLicense()).active;
|
||||
const license = await getEnterpriseLicense();
|
||||
|
||||
if (IS_FORMBRICKS_CLOUD) return billingPlan !== PROJECT_FEATURE_KEYS.FREE && license.active;
|
||||
else if (!IS_FORMBRICKS_CLOUD) return license.active;
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getMultiLanguagePermission = async (
|
||||
billingPlan: Organization["billing"]["plan"]
|
||||
): Promise<boolean> => {
|
||||
if (E2E_TESTING) {
|
||||
const previousResult = await fetchLicenseForE2ETesting();
|
||||
return previousResult && previousResult.active !== null ? previousResult.active : false;
|
||||
}
|
||||
const license = await getEnterpriseLicense();
|
||||
|
||||
if (IS_FORMBRICKS_CLOUD)
|
||||
return billingPlan === PROJECT_FEATURE_KEYS.SCALE || billingPlan === PROJECT_FEATURE_KEYS.ENTERPRISE;
|
||||
else if (!IS_FORMBRICKS_CLOUD) return (await getEnterpriseLicense()).active;
|
||||
return (
|
||||
license.active &&
|
||||
(billingPlan === PROJECT_FEATURE_KEYS.SCALE || billingPlan === PROJECT_FEATURE_KEYS.ENTERPRISE)
|
||||
);
|
||||
else if (!IS_FORMBRICKS_CLOUD) return license.active;
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getIsMultiOrgEnabled = async (): Promise<boolean> => {
|
||||
if (E2E_TESTING) {
|
||||
const previousResult = await fetchLicenseForE2ETesting();
|
||||
return previousResult && previousResult.features ? previousResult.features.isMultiOrgEnabled : false;
|
||||
}
|
||||
// Helper function for simple boolean feature flags
|
||||
const getSpecificFeatureFlag = async (
|
||||
featureKey: keyof Pick<
|
||||
TEnterpriseLicenseFeatures,
|
||||
"isMultiOrgEnabled" | "contacts" | "twoFactorAuth" | "sso"
|
||||
>
|
||||
): Promise<boolean> => {
|
||||
const licenseFeatures = await getLicenseFeatures();
|
||||
if (!licenseFeatures) return false;
|
||||
return licenseFeatures.isMultiOrgEnabled;
|
||||
return typeof licenseFeatures[featureKey] === "boolean" ? licenseFeatures[featureKey] : false;
|
||||
};
|
||||
|
||||
export const getIsMultiOrgEnabled = async (): Promise<boolean> => {
|
||||
return getSpecificFeatureFlag("isMultiOrgEnabled");
|
||||
};
|
||||
|
||||
export const getIsContactsEnabled = async (): Promise<boolean> => {
|
||||
if (E2E_TESTING) {
|
||||
const previousResult = await fetchLicenseForE2ETesting();
|
||||
return previousResult && previousResult.features ? previousResult.features.contacts : false;
|
||||
}
|
||||
const licenseFeatures = await getLicenseFeatures();
|
||||
if (!licenseFeatures) return false;
|
||||
return licenseFeatures.contacts;
|
||||
return getSpecificFeatureFlag("contacts");
|
||||
};
|
||||
|
||||
export const getIsTwoFactorAuthEnabled = async (): Promise<boolean> => {
|
||||
if (E2E_TESTING) {
|
||||
const previousResult = await fetchLicenseForE2ETesting();
|
||||
return previousResult && previousResult.features ? previousResult.features.twoFactorAuth : false;
|
||||
}
|
||||
const licenseFeatures = await getLicenseFeatures();
|
||||
if (!licenseFeatures) return false;
|
||||
return licenseFeatures.twoFactorAuth;
|
||||
return getSpecificFeatureFlag("twoFactorAuth");
|
||||
};
|
||||
|
||||
export const getisSsoEnabled = async (): Promise<boolean> => {
|
||||
if (E2E_TESTING) {
|
||||
const previousResult = await fetchLicenseForE2ETesting();
|
||||
return previousResult && previousResult.features ? previousResult.features.sso : false;
|
||||
}
|
||||
const licenseFeatures = await getLicenseFeatures();
|
||||
if (!licenseFeatures) return false;
|
||||
return licenseFeatures.sso;
|
||||
export const getIsSsoEnabled = async (): Promise<boolean> => {
|
||||
return getSpecificFeatureFlag("sso");
|
||||
};
|
||||
|
||||
export const getIsSamlSsoEnabled = async (): Promise<boolean> => {
|
||||
if (E2E_TESTING) {
|
||||
const previousResult = await fetchLicenseForE2ETesting();
|
||||
return previousResult && previousResult.features
|
||||
? previousResult.features.sso && previousResult.features.saml
|
||||
: false;
|
||||
}
|
||||
if (IS_FORMBRICKS_CLOUD) {
|
||||
return false;
|
||||
}
|
||||
@@ -396,38 +110,30 @@ export const getIsSpamProtectionEnabled = async (
|
||||
): Promise<boolean> => {
|
||||
if (!IS_RECAPTCHA_CONFIGURED) return false;
|
||||
|
||||
if (E2E_TESTING) {
|
||||
const previousResult = await fetchLicenseForE2ETesting();
|
||||
return previousResult?.features ? previousResult.features.spamProtection : false;
|
||||
}
|
||||
if (IS_FORMBRICKS_CLOUD)
|
||||
return billingPlan === PROJECT_FEATURE_KEYS.SCALE || billingPlan === PROJECT_FEATURE_KEYS.ENTERPRISE;
|
||||
const license = await getEnterpriseLicense();
|
||||
|
||||
const licenseFeatures = await getLicenseFeatures();
|
||||
if (!licenseFeatures) return false;
|
||||
return licenseFeatures.spamProtection;
|
||||
if (IS_FORMBRICKS_CLOUD) {
|
||||
return (
|
||||
license.active &&
|
||||
!!license.features?.spamProtection &&
|
||||
(billingPlan === PROJECT_FEATURE_KEYS.SCALE || billingPlan === PROJECT_FEATURE_KEYS.ENTERPRISE)
|
||||
);
|
||||
}
|
||||
|
||||
return license.active && !!license.features?.spamProtection;
|
||||
};
|
||||
|
||||
export const getOrganizationProjectsLimit = async (
|
||||
limits: Organization["billing"]["limits"]
|
||||
): Promise<number> => {
|
||||
if (E2E_TESTING) {
|
||||
const previousResult = await fetchLicenseForE2ETesting();
|
||||
return previousResult && previousResult.features ? (previousResult.features.projects ?? Infinity) : 3;
|
||||
}
|
||||
const license = await getEnterpriseLicense();
|
||||
|
||||
let limit: number;
|
||||
|
||||
if (IS_FORMBRICKS_CLOUD && (await getEnterpriseLicense()).active) {
|
||||
limit = limits.projects ?? Infinity;
|
||||
if (IS_FORMBRICKS_CLOUD) {
|
||||
limit = license.active ? (limits.projects ?? Infinity) : 3;
|
||||
} else {
|
||||
const licenseFeatures = await getLicenseFeatures();
|
||||
if (!licenseFeatures) {
|
||||
limit = 3;
|
||||
} else {
|
||||
limit = licenseFeatures.projects ?? Infinity;
|
||||
}
|
||||
limit = license.active && license.features?.projects != null ? license.features.projects : 3;
|
||||
}
|
||||
|
||||
return limit;
|
||||
};
|
||||
|
||||
@@ -5,15 +5,14 @@ import { verifyInviteToken } from "@/lib/jwt";
|
||||
import { createMembership } from "@/lib/membership/service";
|
||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||
import { createBrevoCustomer } from "@/modules/auth/lib/brevo";
|
||||
import { getUserByEmail, updateUser } from "@/modules/auth/lib/user";
|
||||
import { createUser } from "@/modules/auth/lib/user";
|
||||
import { createUser, getUserByEmail, updateUser } from "@/modules/auth/lib/user";
|
||||
import { getIsValidInviteToken } from "@/modules/auth/signup/lib/invite";
|
||||
import { TOidcNameFields, TSamlNameFields } from "@/modules/auth/types/auth";
|
||||
import {
|
||||
getIsMultiOrgEnabled,
|
||||
getIsSamlSsoEnabled,
|
||||
getIsSsoEnabled,
|
||||
getRoleManagementPermission,
|
||||
getisSsoEnabled,
|
||||
} from "@/modules/ee/license-check/lib/utils";
|
||||
import { getFirstOrganization } from "@/modules/ee/sso/lib/organization";
|
||||
import { createDefaultTeamMembership, getOrganizationByTeamId } from "@/modules/ee/sso/lib/team";
|
||||
@@ -32,7 +31,7 @@ export const handleSsoCallback = async ({
|
||||
account: Account;
|
||||
callbackUrl: string;
|
||||
}) => {
|
||||
const isSsoEnabled = await getisSsoEnabled();
|
||||
const isSsoEnabled = await getIsSsoEnabled();
|
||||
if (!isSsoEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import type { TSamlNameFields } from "@/modules/auth/types/auth";
|
||||
import {
|
||||
getIsMultiOrgEnabled,
|
||||
getIsSamlSsoEnabled,
|
||||
getIsSsoEnabled,
|
||||
getRoleManagementPermission,
|
||||
getisSsoEnabled,
|
||||
} from "@/modules/ee/license-check/lib/utils";
|
||||
import { createDefaultTeamMembership, getOrganizationByTeamId } from "@/modules/ee/sso/lib/team";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
@@ -42,7 +42,7 @@ vi.mock("@/modules/auth/signup/lib/invite", () => ({
|
||||
|
||||
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
|
||||
getIsSamlSsoEnabled: vi.fn(),
|
||||
getisSsoEnabled: vi.fn(),
|
||||
getIsSsoEnabled: vi.fn(),
|
||||
getRoleManagementPermission: vi.fn(),
|
||||
getIsMultiOrgEnabled: vi.fn(),
|
||||
}));
|
||||
@@ -102,7 +102,7 @@ describe("handleSsoCallback", () => {
|
||||
vi.resetModules();
|
||||
|
||||
// Default mock implementations
|
||||
vi.mocked(getisSsoEnabled).mockResolvedValue(true);
|
||||
vi.mocked(getIsSsoEnabled).mockResolvedValue(true);
|
||||
vi.mocked(getIsSamlSsoEnabled).mockResolvedValue(true);
|
||||
vi.mocked(findMatchingLocale).mockResolvedValue("en-US");
|
||||
vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true);
|
||||
@@ -122,7 +122,7 @@ describe("handleSsoCallback", () => {
|
||||
|
||||
describe("Early return conditions", () => {
|
||||
test("should return false if SSO is not enabled", async () => {
|
||||
vi.mocked(getisSsoEnabled).mockResolvedValue(false);
|
||||
vi.mocked(getIsSsoEnabled).mockResolvedValue(false);
|
||||
|
||||
const result = await handleSsoCallback({
|
||||
user: mockUser,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||
import { getIsSamlSsoEnabled, getisSsoEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getIsSamlSsoEnabled, getIsSsoEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
@@ -54,7 +54,7 @@ vi.mock("@/lib/constants", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
|
||||
getisSsoEnabled: vi.fn(),
|
||||
getIsSsoEnabled: vi.fn(),
|
||||
getIsSamlSsoEnabled: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -77,7 +77,7 @@ vi.mock("@/modules/auth/signup/components/signup-form", () => ({
|
||||
|
||||
describe("SignupPage", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(getisSsoEnabled).mockResolvedValue(true);
|
||||
vi.mocked(getIsSsoEnabled).mockResolvedValue(true);
|
||||
vi.mocked(getIsSamlSsoEnabled).mockResolvedValue(false);
|
||||
vi.mocked(findMatchingLocale).mockResolvedValue("en-US");
|
||||
vi.mocked(getTranslate).mockResolvedValue((key) => key);
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from "@/lib/constants";
|
||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||
import { SignupForm } from "@/modules/auth/signup/components/signup-form";
|
||||
import { getIsSamlSsoEnabled, getisSsoEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getIsSamlSsoEnabled, getIsSsoEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { Metadata } from "next";
|
||||
|
||||
@@ -29,7 +29,7 @@ export const metadata: Metadata = {
|
||||
export const SignupPage = async () => {
|
||||
const locale = await findMatchingLocale();
|
||||
|
||||
const [isSsoEnabled, isSamlSsoEnabled] = await Promise.all([getisSsoEnabled(), getIsSamlSsoEnabled()]);
|
||||
const [isSsoEnabled, isSamlSsoEnabled] = await Promise.all([getIsSsoEnabled(), getIsSamlSsoEnabled()]);
|
||||
|
||||
const samlSsoEnabled = isSamlSsoEnabled && SAML_OAUTH_ENABLED;
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"@hookform/resolvers": "5.0.1",
|
||||
"@intercom/messenger-js-sdk": "0.0.14",
|
||||
"@json2csv/node": "7.0.6",
|
||||
"@keyv/redis": "4.4.0",
|
||||
"@lexical/code": "0.31.0",
|
||||
"@lexical/link": "0.31.0",
|
||||
"@lexical/list": "0.31.0",
|
||||
@@ -83,6 +84,7 @@
|
||||
"@vercel/og": "0.6.8",
|
||||
"bcryptjs": "3.0.2",
|
||||
"boring-avatars": "1.11.2",
|
||||
"cache-manager": "6.4.3",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
@@ -95,6 +97,7 @@
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"jiti": "2.4.2",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"keyv": "5.3.3",
|
||||
"lexical": "0.31.0",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "11.1.0",
|
||||
|
||||
@@ -66,8 +66,5 @@
|
||||
"budgetPercentIncreaseRed": 20,
|
||||
"minimumChangeThreshold": 0,
|
||||
"showDetails": true
|
||||
},
|
||||
"dependencies": {
|
||||
"@changesets/cli": "2.29.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Pino, { type Logger, type LoggerOptions, stdSerializers } from "pino";
|
||||
import { LOG_LEVELS, type TLogLevel, ZLogLevel } from "../types/logger";
|
||||
import { type TLogLevel, ZLogLevel } from "../types/logger";
|
||||
|
||||
const IS_PRODUCTION = !process.env.NODE_ENV || process.env.NODE_ENV === "production";
|
||||
const getLogLevel = (): TLogLevel => {
|
||||
@@ -58,12 +58,17 @@ const productionConfig: LoggerOptions = {
|
||||
|
||||
const logger: Logger = IS_PRODUCTION ? Pino(productionConfig) : Pino(developmentConfig);
|
||||
|
||||
LOG_LEVELS.forEach((level) => {
|
||||
logger[level] = logger[level].bind(logger);
|
||||
});
|
||||
// Ensure all log levels are properly bound
|
||||
const boundLogger = {
|
||||
debug: logger.debug.bind(logger),
|
||||
info: logger.info.bind(logger),
|
||||
warn: logger.warn.bind(logger),
|
||||
error: logger.error.bind(logger),
|
||||
fatal: logger.fatal.bind(logger),
|
||||
};
|
||||
|
||||
const extendedLogger = {
|
||||
...logger,
|
||||
...boundLogger,
|
||||
withContext: (context: Record<string, unknown>) => logger.child(context),
|
||||
request: (req: Request) =>
|
||||
logger.child({
|
||||
|
||||
32
packages/surveys/src/lib/i18n.test.ts
Normal file
32
packages/surveys/src/lib/i18n.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { TI18nString } from "@formbricks/types/surveys/types";
|
||||
import { getLocalizedValue } from "./i18n";
|
||||
|
||||
describe("i18n", () => {
|
||||
describe("getLocalizedValue", () => {
|
||||
test("should return empty string for undefined value", () => {
|
||||
expect(getLocalizedValue(undefined, "en")).toBe("");
|
||||
});
|
||||
|
||||
test("should return empty string for non-i18n string", () => {
|
||||
expect(getLocalizedValue("not an i18n string" as any, "en")).toBe("");
|
||||
});
|
||||
|
||||
test("should return default value when language not found", () => {
|
||||
const i18nString: TI18nString = {
|
||||
default: "Default text",
|
||||
en: "English text",
|
||||
};
|
||||
expect(getLocalizedValue(i18nString, "fr")).toBe("Default text");
|
||||
});
|
||||
|
||||
test("should return localized value when language found", () => {
|
||||
const i18nString: TI18nString = {
|
||||
default: "Default text",
|
||||
en: "English text",
|
||||
fr: "French text",
|
||||
};
|
||||
expect(getLocalizedValue(i18nString, "fr")).toBe("French text");
|
||||
});
|
||||
});
|
||||
});
|
||||
484
pnpm-lock.yaml
generated
484
pnpm-lock.yaml
generated
@@ -7,10 +7,6 @@ settings:
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@changesets/cli':
|
||||
specifier: 2.29.3
|
||||
version: 2.29.3
|
||||
devDependencies:
|
||||
'@azure/microsoft-playwright-testing':
|
||||
specifier: 1.0.0-beta.7
|
||||
@@ -163,6 +159,9 @@ importers:
|
||||
'@json2csv/node':
|
||||
specifier: 7.0.6
|
||||
version: 7.0.6
|
||||
'@keyv/redis':
|
||||
specifier: 4.4.0
|
||||
version: 4.4.0(keyv@5.3.3)
|
||||
'@lexical/code':
|
||||
specifier: 0.31.0
|
||||
version: 0.31.0
|
||||
@@ -304,6 +303,9 @@ importers:
|
||||
boring-avatars:
|
||||
specifier: 1.11.2
|
||||
version: 1.11.2
|
||||
cache-manager:
|
||||
specifier: 6.4.3
|
||||
version: 6.4.3
|
||||
class-variance-authority:
|
||||
specifier: 0.7.1
|
||||
version: 0.7.1
|
||||
@@ -340,6 +342,9 @@ importers:
|
||||
jsonwebtoken:
|
||||
specifier: 9.0.2
|
||||
version: 9.0.2
|
||||
keyv:
|
||||
specifier: 5.3.3
|
||||
version: 5.3.3
|
||||
lexical:
|
||||
specifier: 0.31.0
|
||||
version: 0.31.0
|
||||
@@ -1342,61 +1347,6 @@ packages:
|
||||
'@calcom/embed-snippet@1.3.3':
|
||||
resolution: {integrity: sha512-pqqKaeLB8R6BvyegcpI9gAyY6Xyx1bKYfWvIGOvIbTpguWyM1BBBVcT9DCeGe8Zw7Ujp5K56ci7isRUrT2Uadg==}
|
||||
|
||||
'@changesets/apply-release-plan@7.0.12':
|
||||
resolution: {integrity: sha512-EaET7As5CeuhTzvXTQCRZeBUcisoYPDDcXvgTE/2jmmypKp0RC7LxKj/yzqeh/1qFTZI7oDGFcL1PHRuQuketQ==}
|
||||
|
||||
'@changesets/assemble-release-plan@6.0.7':
|
||||
resolution: {integrity: sha512-vS5J92Rm7ZUcrvtu6WvggGWIdohv8s1/3ypRYQX8FsPO+KPDx6JaNC3YwSfh2umY/faGGfNnq42A7PRT0aZPFw==}
|
||||
|
||||
'@changesets/changelog-git@0.2.1':
|
||||
resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==}
|
||||
|
||||
'@changesets/cli@2.29.3':
|
||||
resolution: {integrity: sha512-TNhKr6Loc7I0CSD9LpAyVNSxWBHElXVmmvQYIZQvaMan5jddmL7geo3+08Wi7ImgHFVNB0Nhju/LzXqlrkoOxg==}
|
||||
hasBin: true
|
||||
|
||||
'@changesets/config@3.1.1':
|
||||
resolution: {integrity: sha512-bd+3Ap2TKXxljCggI0mKPfzCQKeV/TU4yO2h2C6vAihIo8tzseAn2e7klSuiyYYXvgu53zMN1OeYMIQkaQoWnA==}
|
||||
|
||||
'@changesets/errors@0.2.0':
|
||||
resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==}
|
||||
|
||||
'@changesets/get-dependents-graph@2.1.3':
|
||||
resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==}
|
||||
|
||||
'@changesets/get-release-plan@4.0.11':
|
||||
resolution: {integrity: sha512-4DZpsewsc/1m5TArVg5h1c0U94am+cJBnu3izAM3yYIZr8+zZwa3AXYdEyCNURzjx0wWr80u/TWoxshbwdZXOA==}
|
||||
|
||||
'@changesets/get-version-range-type@0.4.0':
|
||||
resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==}
|
||||
|
||||
'@changesets/git@3.0.4':
|
||||
resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==}
|
||||
|
||||
'@changesets/logger@0.1.1':
|
||||
resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==}
|
||||
|
||||
'@changesets/parse@0.4.1':
|
||||
resolution: {integrity: sha512-iwksMs5Bf/wUItfcg+OXrEpravm5rEd9Bf4oyIPL4kVTmJQ7PNDSd6MDYkpSJR1pn7tz/k8Zf2DhTCqX08Ou+Q==}
|
||||
|
||||
'@changesets/pre@2.0.2':
|
||||
resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==}
|
||||
|
||||
'@changesets/read@0.6.5':
|
||||
resolution: {integrity: sha512-UPzNGhsSjHD3Veb0xO/MwvasGe8eMyNrR/sT9gR8Q3DhOQZirgKhhXv/8hVsI0QpPjR004Z9iFxoJU6in3uGMg==}
|
||||
|
||||
'@changesets/should-skip-package@0.1.2':
|
||||
resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==}
|
||||
|
||||
'@changesets/types@4.1.0':
|
||||
resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==}
|
||||
|
||||
'@changesets/types@6.1.0':
|
||||
resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==}
|
||||
|
||||
'@changesets/write@0.4.0':
|
||||
resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==}
|
||||
|
||||
'@chromatic-com/storybook@3.2.6':
|
||||
resolution: {integrity: sha512-FDmn5Ry2DzQdik+eq2sp/kJMMT36Ewe7ONXUXM2Izd97c7r6R/QyGli8eyh/F0iyqVvbLveNYFyF0dBOJNwLqw==}
|
||||
engines: {node: '>=16.0.0', yarn: '>=1.22.18'}
|
||||
@@ -1894,6 +1844,15 @@ packages:
|
||||
'@json2csv/plainjs@7.0.6':
|
||||
resolution: {integrity: sha512-4Md7RPDCSYpmW1HWIpWBOqCd4vWfIqm53S3e/uzQ62iGi7L3r34fK/8nhOMEe+/eVfCx8+gdSCt1d74SlacQHw==}
|
||||
|
||||
'@keyv/redis@4.4.0':
|
||||
resolution: {integrity: sha512-n/KEj3S7crVkoykggqsMUtcjNGvjagGPlJYgO/r6m9hhGZfhp1txJElHxcdJ1ANi/LJoBuOSILj15g6HD2ucqQ==}
|
||||
engines: {node: '>= 18'}
|
||||
peerDependencies:
|
||||
keyv: ^5.3.3
|
||||
|
||||
'@keyv/serialize@1.0.3':
|
||||
resolution: {integrity: sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g==}
|
||||
|
||||
'@lexical/clipboard@0.31.0':
|
||||
resolution: {integrity: sha512-oOZgDnzD68uZMy99H1dx6Le/uJNxmvXgbHb2Qp6Zrej7OUnCV/X9kzVSgLqWKytgrjsyUjTLjRhkQgwLhu61sA==}
|
||||
|
||||
@@ -1977,12 +1936,6 @@ packages:
|
||||
'@libsql/sqlite3@0.3.1':
|
||||
resolution: {integrity: sha512-KOOBUuKDjqzteM6QA0W1vnZDfOt5MNMyo2yr4TaH1RcCd7Fsts4lpzfty6FmE1d6QDrRxjRq2ZciO6VdQ9ZF3A==}
|
||||
|
||||
'@manypkg/find-root@1.1.0':
|
||||
resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==}
|
||||
|
||||
'@manypkg/get-packages@1.1.3':
|
||||
resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==}
|
||||
|
||||
'@mdx-js/react@3.1.0':
|
||||
resolution: {integrity: sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==}
|
||||
peerDependencies:
|
||||
@@ -4227,9 +4180,6 @@ packages:
|
||||
'@types/node-fetch@2.6.12':
|
||||
resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==}
|
||||
|
||||
'@types/node@12.20.55':
|
||||
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
|
||||
|
||||
'@types/node@22.14.0':
|
||||
resolution: {integrity: sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==}
|
||||
|
||||
@@ -5064,10 +5014,6 @@ packages:
|
||||
resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
better-path-resolve@1.0.0:
|
||||
resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
big.js@5.2.2:
|
||||
resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
|
||||
|
||||
@@ -5161,6 +5107,9 @@ packages:
|
||||
resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
cache-manager@6.4.3:
|
||||
resolution: {integrity: sha512-VV5eq/QQ5rIVix7/aICO4JyvSeEv9eIQuKL5iFwgM2BrcYoE0A/D1mNsAHJAsB0WEbNdBlKkn6Tjz6fKzh/cKQ==}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -5227,9 +5176,6 @@ packages:
|
||||
resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==}
|
||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||
|
||||
chardet@0.7.0:
|
||||
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
|
||||
|
||||
check-error@2.1.1:
|
||||
resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
|
||||
engines: {node: '>= 16'}
|
||||
@@ -5261,10 +5207,6 @@ packages:
|
||||
resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
|
||||
ci-info@3.9.0:
|
||||
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ci-info@4.2.0:
|
||||
resolution: {integrity: sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -5653,10 +5595,6 @@ packages:
|
||||
detect-europe-js@0.1.2:
|
||||
resolution: {integrity: sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==}
|
||||
|
||||
detect-indent@6.1.0:
|
||||
resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
detect-indent@7.0.1:
|
||||
resolution: {integrity: sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==}
|
||||
engines: {node: '>=12.20'}
|
||||
@@ -5781,10 +5719,6 @@ packages:
|
||||
resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
enquirer@2.4.1:
|
||||
resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
entities@4.5.0:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
engines: {node: '>=0.12'}
|
||||
@@ -6147,13 +6081,6 @@ packages:
|
||||
extend@3.0.2:
|
||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||
|
||||
extendable-error@0.1.7:
|
||||
resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==}
|
||||
|
||||
external-editor@3.1.0:
|
||||
resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
fast-copy@3.0.2:
|
||||
resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==}
|
||||
|
||||
@@ -6314,14 +6241,6 @@ packages:
|
||||
resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==}
|
||||
engines: {node: '>=14.14'}
|
||||
|
||||
fs-extra@7.0.1:
|
||||
resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
|
||||
engines: {node: '>=6 <7 || >=8'}
|
||||
|
||||
fs-extra@8.1.0:
|
||||
resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
|
||||
engines: {node: '>=6 <7 || >=8'}
|
||||
|
||||
fs-minipass@2.1.0:
|
||||
resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -6606,10 +6525,6 @@ packages:
|
||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
human-id@4.1.1:
|
||||
resolution: {integrity: sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==}
|
||||
hasBin: true
|
||||
|
||||
human-signals@2.1.0:
|
||||
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
||||
engines: {node: '>=10.17.0'}
|
||||
@@ -6629,10 +6544,6 @@ packages:
|
||||
hyphenate-style-name@1.1.0:
|
||||
resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==}
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -6858,10 +6769,6 @@ packages:
|
||||
resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-subdir@1.2.0:
|
||||
resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
is-symbol@1.1.1:
|
||||
resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -6886,10 +6793,6 @@ packages:
|
||||
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
|
||||
engines: {node: '>=12.13'}
|
||||
|
||||
is-windows@1.0.2:
|
||||
resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-wsl@2.2.0:
|
||||
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -6998,10 +6901,6 @@ packages:
|
||||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
js-yaml@3.14.1:
|
||||
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
|
||||
hasBin: true
|
||||
|
||||
js-yaml@4.1.0:
|
||||
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
|
||||
hasBin: true
|
||||
@@ -7058,9 +6957,6 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
jsonfile@4.0.0:
|
||||
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
|
||||
|
||||
jsonfile@6.1.0:
|
||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||
|
||||
@@ -7090,6 +6986,9 @@ packages:
|
||||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
keyv@5.3.3:
|
||||
resolution: {integrity: sha512-Rwu4+nXI9fqcxiEHtbkvoes2X+QfkTRo1TMkPfwzipGsJlJO/z69vqB4FNl9xJ3xCpAcbkvmEabZfPzrwN3+gQ==}
|
||||
|
||||
kolorist@1.8.0:
|
||||
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
|
||||
|
||||
@@ -7259,9 +7158,6 @@ packages:
|
||||
lodash.once@4.1.1:
|
||||
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
||||
|
||||
lodash.startcase@4.4.0:
|
||||
resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==}
|
||||
|
||||
lodash@4.17.21:
|
||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||
|
||||
@@ -7572,10 +7468,6 @@ packages:
|
||||
engines: {node: '>=10.0.0'}
|
||||
hasBin: true
|
||||
|
||||
mri@1.2.0:
|
||||
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
ms@2.0.0:
|
||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||
|
||||
@@ -7884,16 +7776,9 @@ packages:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
os-tmpdir@1.0.2:
|
||||
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
otplib@12.0.1:
|
||||
resolution: {integrity: sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==}
|
||||
|
||||
outdent@0.5.0:
|
||||
resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==}
|
||||
|
||||
own-keys@1.0.1:
|
||||
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -7902,10 +7787,6 @@ packages:
|
||||
resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
p-filter@2.1.0:
|
||||
resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
p-limit@2.3.0:
|
||||
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -7922,10 +7803,6 @@ packages:
|
||||
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
p-map@2.1.0:
|
||||
resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
p-map@4.0.0:
|
||||
resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -7937,9 +7814,6 @@ packages:
|
||||
package-json-from-dist@1.0.1:
|
||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||
|
||||
package-manager-detector@0.2.11:
|
||||
resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==}
|
||||
|
||||
pako@0.2.9:
|
||||
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
|
||||
|
||||
@@ -8070,10 +7944,6 @@ packages:
|
||||
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
pify@4.0.1:
|
||||
resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
pino-abstract-transport@2.0.0:
|
||||
resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==}
|
||||
|
||||
@@ -8301,11 +8171,6 @@ packages:
|
||||
prettier-plugin-svelte:
|
||||
optional: true
|
||||
|
||||
prettier@2.8.8:
|
||||
resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
hasBin: true
|
||||
|
||||
prettier@3.5.3:
|
||||
resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -8586,10 +8451,6 @@ packages:
|
||||
resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
read-yaml-file@1.1.0:
|
||||
resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
readable-stream@2.3.8:
|
||||
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||
|
||||
@@ -8674,10 +8535,6 @@ packages:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
resolve-from@5.0.0:
|
||||
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
resolve-pkg-maps@1.0.0:
|
||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||
|
||||
@@ -8979,9 +8836,6 @@ packages:
|
||||
sparse-bitfield@3.0.3:
|
||||
resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==}
|
||||
|
||||
spawndamnit@3.0.1:
|
||||
resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==}
|
||||
|
||||
spdx-correct@3.2.0:
|
||||
resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==}
|
||||
|
||||
@@ -9257,10 +9111,6 @@ packages:
|
||||
resolution: {integrity: sha512-9AvErXXQTd6l7TDd5EmM+nxbOGyhnmdbp/8c3pw+tjaiSXW9usME90ET/CRG1LN1Y9tPMtz/p83z4Q97B4DDpw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
term-size@2.2.1:
|
||||
resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
terser-webpack-plugin@5.3.14:
|
||||
resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
@@ -9346,10 +9196,6 @@ packages:
|
||||
resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==}
|
||||
hasBin: true
|
||||
|
||||
tmp@0.0.33:
|
||||
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
|
||||
engines: {node: '>=0.6.0'}
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
@@ -9634,10 +9480,6 @@ packages:
|
||||
unique-slug@2.0.2:
|
||||
resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==}
|
||||
|
||||
universalify@0.1.2:
|
||||
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
|
||||
engines: {node: '>= 4.0.0'}
|
||||
|
||||
universalify@2.0.1:
|
||||
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
@@ -11436,148 +11278,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@calcom/embed-core': 1.5.3
|
||||
|
||||
'@changesets/apply-release-plan@7.0.12':
|
||||
dependencies:
|
||||
'@changesets/config': 3.1.1
|
||||
'@changesets/get-version-range-type': 0.4.0
|
||||
'@changesets/git': 3.0.4
|
||||
'@changesets/should-skip-package': 0.1.2
|
||||
'@changesets/types': 6.1.0
|
||||
'@manypkg/get-packages': 1.1.3
|
||||
detect-indent: 6.1.0
|
||||
fs-extra: 7.0.1
|
||||
lodash.startcase: 4.4.0
|
||||
outdent: 0.5.0
|
||||
prettier: 2.8.8
|
||||
resolve-from: 5.0.0
|
||||
semver: 7.7.1
|
||||
|
||||
'@changesets/assemble-release-plan@6.0.7':
|
||||
dependencies:
|
||||
'@changesets/errors': 0.2.0
|
||||
'@changesets/get-dependents-graph': 2.1.3
|
||||
'@changesets/should-skip-package': 0.1.2
|
||||
'@changesets/types': 6.1.0
|
||||
'@manypkg/get-packages': 1.1.3
|
||||
semver: 7.7.1
|
||||
|
||||
'@changesets/changelog-git@0.2.1':
|
||||
dependencies:
|
||||
'@changesets/types': 6.1.0
|
||||
|
||||
'@changesets/cli@2.29.3':
|
||||
dependencies:
|
||||
'@changesets/apply-release-plan': 7.0.12
|
||||
'@changesets/assemble-release-plan': 6.0.7
|
||||
'@changesets/changelog-git': 0.2.1
|
||||
'@changesets/config': 3.1.1
|
||||
'@changesets/errors': 0.2.0
|
||||
'@changesets/get-dependents-graph': 2.1.3
|
||||
'@changesets/get-release-plan': 4.0.11
|
||||
'@changesets/git': 3.0.4
|
||||
'@changesets/logger': 0.1.1
|
||||
'@changesets/pre': 2.0.2
|
||||
'@changesets/read': 0.6.5
|
||||
'@changesets/should-skip-package': 0.1.2
|
||||
'@changesets/types': 6.1.0
|
||||
'@changesets/write': 0.4.0
|
||||
'@manypkg/get-packages': 1.1.3
|
||||
ansi-colors: 4.1.3
|
||||
ci-info: 3.9.0
|
||||
enquirer: 2.4.1
|
||||
external-editor: 3.1.0
|
||||
fs-extra: 7.0.1
|
||||
mri: 1.2.0
|
||||
p-limit: 2.3.0
|
||||
package-manager-detector: 0.2.11
|
||||
picocolors: 1.1.1
|
||||
resolve-from: 5.0.0
|
||||
semver: 7.7.1
|
||||
spawndamnit: 3.0.1
|
||||
term-size: 2.2.1
|
||||
|
||||
'@changesets/config@3.1.1':
|
||||
dependencies:
|
||||
'@changesets/errors': 0.2.0
|
||||
'@changesets/get-dependents-graph': 2.1.3
|
||||
'@changesets/logger': 0.1.1
|
||||
'@changesets/types': 6.1.0
|
||||
'@manypkg/get-packages': 1.1.3
|
||||
fs-extra: 7.0.1
|
||||
micromatch: 4.0.8
|
||||
|
||||
'@changesets/errors@0.2.0':
|
||||
dependencies:
|
||||
extendable-error: 0.1.7
|
||||
|
||||
'@changesets/get-dependents-graph@2.1.3':
|
||||
dependencies:
|
||||
'@changesets/types': 6.1.0
|
||||
'@manypkg/get-packages': 1.1.3
|
||||
picocolors: 1.1.1
|
||||
semver: 7.7.1
|
||||
|
||||
'@changesets/get-release-plan@4.0.11':
|
||||
dependencies:
|
||||
'@changesets/assemble-release-plan': 6.0.7
|
||||
'@changesets/config': 3.1.1
|
||||
'@changesets/pre': 2.0.2
|
||||
'@changesets/read': 0.6.5
|
||||
'@changesets/types': 6.1.0
|
||||
'@manypkg/get-packages': 1.1.3
|
||||
|
||||
'@changesets/get-version-range-type@0.4.0': {}
|
||||
|
||||
'@changesets/git@3.0.4':
|
||||
dependencies:
|
||||
'@changesets/errors': 0.2.0
|
||||
'@manypkg/get-packages': 1.1.3
|
||||
is-subdir: 1.2.0
|
||||
micromatch: 4.0.8
|
||||
spawndamnit: 3.0.1
|
||||
|
||||
'@changesets/logger@0.1.1':
|
||||
dependencies:
|
||||
picocolors: 1.1.1
|
||||
|
||||
'@changesets/parse@0.4.1':
|
||||
dependencies:
|
||||
'@changesets/types': 6.1.0
|
||||
js-yaml: 3.14.1
|
||||
|
||||
'@changesets/pre@2.0.2':
|
||||
dependencies:
|
||||
'@changesets/errors': 0.2.0
|
||||
'@changesets/types': 6.1.0
|
||||
'@manypkg/get-packages': 1.1.3
|
||||
fs-extra: 7.0.1
|
||||
|
||||
'@changesets/read@0.6.5':
|
||||
dependencies:
|
||||
'@changesets/git': 3.0.4
|
||||
'@changesets/logger': 0.1.1
|
||||
'@changesets/parse': 0.4.1
|
||||
'@changesets/types': 6.1.0
|
||||
fs-extra: 7.0.1
|
||||
p-filter: 2.1.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
'@changesets/should-skip-package@0.1.2':
|
||||
dependencies:
|
||||
'@changesets/types': 6.1.0
|
||||
'@manypkg/get-packages': 1.1.3
|
||||
|
||||
'@changesets/types@4.1.0': {}
|
||||
|
||||
'@changesets/types@6.1.0': {}
|
||||
|
||||
'@changesets/write@0.4.0':
|
||||
dependencies:
|
||||
'@changesets/types': 6.1.0
|
||||
fs-extra: 7.0.1
|
||||
human-id: 4.1.1
|
||||
prettier: 2.8.8
|
||||
|
||||
'@chromatic-com/storybook@3.2.6(react@19.1.0)(storybook@8.6.12(prettier@3.5.3))':
|
||||
dependencies:
|
||||
chromatic: 11.28.2
|
||||
@@ -12019,6 +11719,16 @@ snapshots:
|
||||
'@json2csv/formatters': 7.0.6
|
||||
'@streamparser/json': 0.0.20
|
||||
|
||||
'@keyv/redis@4.4.0(keyv@5.3.3)':
|
||||
dependencies:
|
||||
'@redis/client': 1.6.0
|
||||
cluster-key-slot: 1.1.2
|
||||
keyv: 5.3.3
|
||||
|
||||
'@keyv/serialize@1.0.3':
|
||||
dependencies:
|
||||
buffer: 6.0.3
|
||||
|
||||
'@lexical/clipboard@0.31.0':
|
||||
dependencies:
|
||||
'@lexical/html': 0.31.0
|
||||
@@ -12197,22 +11907,6 @@ snapshots:
|
||||
- encoding
|
||||
- utf-8-validate
|
||||
|
||||
'@manypkg/find-root@1.1.0':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.0
|
||||
'@types/node': 12.20.55
|
||||
find-up: 4.1.0
|
||||
fs-extra: 8.1.0
|
||||
|
||||
'@manypkg/get-packages@1.1.3':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.0
|
||||
'@changesets/types': 4.1.0
|
||||
'@manypkg/find-root': 1.1.0
|
||||
fs-extra: 8.1.0
|
||||
globby: 11.1.0
|
||||
read-yaml-file: 1.1.0
|
||||
|
||||
'@mdx-js/react@3.1.0(@types/react@19.1.3)(react@19.1.0)':
|
||||
dependencies:
|
||||
'@types/mdx': 2.0.13
|
||||
@@ -14767,8 +14461,6 @@ snapshots:
|
||||
'@types/node': 22.15.14
|
||||
form-data: 4.0.2
|
||||
|
||||
'@types/node@12.20.55': {}
|
||||
|
||||
'@types/node@22.14.0':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
@@ -15766,10 +15458,6 @@ snapshots:
|
||||
dependencies:
|
||||
open: 8.4.2
|
||||
|
||||
better-path-resolve@1.0.0:
|
||||
dependencies:
|
||||
is-windows: 1.0.2
|
||||
|
||||
big.js@5.2.2: {}
|
||||
|
||||
bignumber.js@9.3.0: {}
|
||||
@@ -15888,6 +15576,10 @@ snapshots:
|
||||
- bluebird
|
||||
optional: true
|
||||
|
||||
cache-manager@6.4.3:
|
||||
dependencies:
|
||||
keyv: 5.3.3
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
@@ -15954,8 +15646,6 @@ snapshots:
|
||||
|
||||
chalk@5.4.1: {}
|
||||
|
||||
chardet@0.7.0: {}
|
||||
|
||||
check-error@2.1.1: {}
|
||||
|
||||
chokidar@3.6.0:
|
||||
@@ -15978,8 +15668,6 @@ snapshots:
|
||||
|
||||
chrome-trace-event@1.0.4: {}
|
||||
|
||||
ci-info@3.9.0: {}
|
||||
|
||||
ci-info@4.2.0: {}
|
||||
|
||||
cjs-module-lexer@1.4.3: {}
|
||||
@@ -16351,8 +16039,6 @@ snapshots:
|
||||
|
||||
detect-europe-js@0.1.2: {}
|
||||
|
||||
detect-indent@6.1.0: {}
|
||||
|
||||
detect-indent@7.0.1: {}
|
||||
|
||||
detect-libc@2.0.3: {}
|
||||
@@ -16473,11 +16159,6 @@ snapshots:
|
||||
graceful-fs: 4.2.11
|
||||
tapable: 2.2.1
|
||||
|
||||
enquirer@2.4.1:
|
||||
dependencies:
|
||||
ansi-colors: 4.1.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
entities@4.5.0: {}
|
||||
|
||||
env-paths@2.2.1: {}
|
||||
@@ -17067,14 +16748,6 @@ snapshots:
|
||||
|
||||
extend@3.0.2: {}
|
||||
|
||||
extendable-error@0.1.7: {}
|
||||
|
||||
external-editor@3.1.0:
|
||||
dependencies:
|
||||
chardet: 0.7.0
|
||||
iconv-lite: 0.4.24
|
||||
tmp: 0.0.33
|
||||
|
||||
fast-copy@3.0.2: {}
|
||||
|
||||
fast-deep-equal@2.0.1: {}
|
||||
@@ -17217,18 +16890,6 @@ snapshots:
|
||||
jsonfile: 6.1.0
|
||||
universalify: 2.0.1
|
||||
|
||||
fs-extra@7.0.1:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
jsonfile: 4.0.0
|
||||
universalify: 0.1.2
|
||||
|
||||
fs-extra@8.1.0:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
jsonfile: 4.0.0
|
||||
universalify: 0.1.2
|
||||
|
||||
fs-minipass@2.1.0:
|
||||
dependencies:
|
||||
minipass: 3.3.6
|
||||
@@ -17582,8 +17243,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
human-id@4.1.1: {}
|
||||
|
||||
human-signals@2.1.0: {}
|
||||
|
||||
human-signals@5.0.0: {}
|
||||
@@ -17597,10 +17256,6 @@ snapshots:
|
||||
|
||||
hyphenate-style-name@1.1.0: {}
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
@@ -17805,10 +17460,6 @@ snapshots:
|
||||
call-bound: 1.0.4
|
||||
has-tostringtag: 1.0.2
|
||||
|
||||
is-subdir@1.2.0:
|
||||
dependencies:
|
||||
better-path-resolve: 1.0.0
|
||||
|
||||
is-symbol@1.1.1:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
@@ -17832,8 +17483,6 @@ snapshots:
|
||||
|
||||
is-what@4.1.16: {}
|
||||
|
||||
is-windows@1.0.2: {}
|
||||
|
||||
is-wsl@2.2.0:
|
||||
dependencies:
|
||||
is-docker: 2.2.1
|
||||
@@ -17948,11 +17597,6 @@ snapshots:
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
js-yaml@3.14.1:
|
||||
dependencies:
|
||||
argparse: 1.0.10
|
||||
esprima: 4.0.1
|
||||
|
||||
js-yaml@4.1.0:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
@@ -18013,10 +17657,6 @@ snapshots:
|
||||
|
||||
json5@2.2.3: {}
|
||||
|
||||
jsonfile@4.0.0:
|
||||
optionalDependencies:
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
jsonfile@6.1.0:
|
||||
dependencies:
|
||||
universalify: 2.0.1
|
||||
@@ -18071,6 +17711,10 @@ snapshots:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
|
||||
keyv@5.3.3:
|
||||
dependencies:
|
||||
'@keyv/serialize': 1.0.3
|
||||
|
||||
kolorist@1.8.0: {}
|
||||
|
||||
language-subtag-registry@0.3.23: {}
|
||||
@@ -18222,8 +17866,6 @@ snapshots:
|
||||
|
||||
lodash.once@4.1.1: {}
|
||||
|
||||
lodash.startcase@4.4.0: {}
|
||||
|
||||
lodash@4.17.21: {}
|
||||
|
||||
log-symbols@2.2.0:
|
||||
@@ -18540,8 +18182,6 @@ snapshots:
|
||||
- utf-8-validate
|
||||
optional: true
|
||||
|
||||
mri@1.2.0: {}
|
||||
|
||||
ms@2.0.0: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
@@ -18878,16 +18518,12 @@ snapshots:
|
||||
type-check: 0.4.0
|
||||
word-wrap: 1.2.5
|
||||
|
||||
os-tmpdir@1.0.2: {}
|
||||
|
||||
otplib@12.0.1:
|
||||
dependencies:
|
||||
'@otplib/core': 12.0.1
|
||||
'@otplib/preset-default': 12.0.1
|
||||
'@otplib/preset-v11': 12.0.1
|
||||
|
||||
outdent@0.5.0: {}
|
||||
|
||||
own-keys@1.0.1:
|
||||
dependencies:
|
||||
get-intrinsic: 1.3.0
|
||||
@@ -18896,10 +18532,6 @@ snapshots:
|
||||
|
||||
p-defer@1.0.0: {}
|
||||
|
||||
p-filter@2.1.0:
|
||||
dependencies:
|
||||
p-map: 2.1.0
|
||||
|
||||
p-limit@2.3.0:
|
||||
dependencies:
|
||||
p-try: 2.2.0
|
||||
@@ -18916,8 +18548,6 @@ snapshots:
|
||||
dependencies:
|
||||
p-limit: 3.1.0
|
||||
|
||||
p-map@2.1.0: {}
|
||||
|
||||
p-map@4.0.0:
|
||||
dependencies:
|
||||
aggregate-error: 3.1.0
|
||||
@@ -18927,10 +18557,6 @@ snapshots:
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
|
||||
package-manager-detector@0.2.11:
|
||||
dependencies:
|
||||
quansync: 0.2.10
|
||||
|
||||
pako@0.2.9: {}
|
||||
|
||||
papaparse@5.5.2: {}
|
||||
@@ -19041,8 +18667,6 @@ snapshots:
|
||||
|
||||
pify@2.3.0: {}
|
||||
|
||||
pify@4.0.1: {}
|
||||
|
||||
pino-abstract-transport@2.0.0:
|
||||
dependencies:
|
||||
split2: 4.2.0
|
||||
@@ -19232,8 +18856,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@trivago/prettier-plugin-sort-imports': 5.2.2(prettier@3.5.3)
|
||||
|
||||
prettier@2.8.8: {}
|
||||
|
||||
prettier@3.5.3: {}
|
||||
|
||||
pretty-format@22.4.3:
|
||||
@@ -19531,13 +19153,6 @@ snapshots:
|
||||
parse-json: 5.2.0
|
||||
type-fest: 0.6.0
|
||||
|
||||
read-yaml-file@1.1.0:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
js-yaml: 3.14.1
|
||||
pify: 4.0.1
|
||||
strip-bom: 3.0.0
|
||||
|
||||
readable-stream@2.3.8:
|
||||
dependencies:
|
||||
core-util-is: 1.0.3
|
||||
@@ -19651,8 +19266,6 @@ snapshots:
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
resolve-from@5.0.0: {}
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
|
||||
resolve@1.19.0:
|
||||
@@ -20067,11 +19680,6 @@ snapshots:
|
||||
dependencies:
|
||||
memory-pager: 1.5.0
|
||||
|
||||
spawndamnit@3.0.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
spdx-correct@3.2.0:
|
||||
dependencies:
|
||||
spdx-expression-parse: 3.0.1
|
||||
@@ -20405,8 +20013,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
term-size@2.2.1: {}
|
||||
|
||||
terser-webpack-plugin@5.3.14(webpack@5.99.8):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
@@ -20474,10 +20080,6 @@ snapshots:
|
||||
dependencies:
|
||||
tldts-core: 6.1.86
|
||||
|
||||
tmp@0.0.33:
|
||||
dependencies:
|
||||
os-tmpdir: 1.0.2
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
dependencies:
|
||||
is-number: 7.0.0
|
||||
@@ -20723,8 +20325,6 @@ snapshots:
|
||||
imurmurhash: 0.1.4
|
||||
optional: true
|
||||
|
||||
universalify@0.1.2: {}
|
||||
|
||||
universalify@2.0.1: {}
|
||||
|
||||
unplugin@1.0.1:
|
||||
|
||||
Reference in New Issue
Block a user