diff --git a/.github/values.yaml b/.github/values.yaml deleted file mode 100644 index 1b3ffd87a..000000000 --- a/.github/values.yaml +++ /dev/null @@ -1,57 +0,0 @@ -## @section Common parameters -## - -## @param nameOverride Override release name -## -nameOverride: "" -## @param fullnameOverride Override release fullname -## -fullnameOverride: "" - -## @section Infisical backend parameters -## Documentation : https://infisical.com/docs/self-hosting/deployments/kubernetes -## - -infisical: - autoDatabaseSchemaMigration: false - - enabled: false - - name: infisical - replicaCount: 3 - image: - repository: infisical/staging_infisical - tag: "latest" - pullPolicy: Always - - deploymentAnnotations: - secrets.infisical.com/auto-reload: "true" - - kubeSecretRef: "managed-secret" - -ingress: - ## @param ingress.enabled Enable ingress - ## - enabled: true - ## @param ingress.ingressClassName Ingress class name - ## - ingressClassName: nginx - ## @param ingress.nginx.enabled Ingress controller - ## - # nginx: - # enabled: true - ## @param ingress.annotations Ingress annotations - ## - annotations: - cert-manager.io/cluster-issuer: "letsencrypt-prod" - hostName: "gamma.infisical.com" - tls: - - secretName: letsencrypt-prod - hosts: - - gamma.infisical.com - -postgresql: - enabled: false - -redis: - enabled: false diff --git a/.github/workflows/helm-release-infisical-core.yml b/.github/workflows/helm-release-infisical-core.yml index 6c317cc27..49118a5ae 100644 --- a/.github/workflows/helm-release-infisical-core.yml +++ b/.github/workflows/helm-release-infisical-core.yml @@ -56,7 +56,7 @@ jobs: --config ct.yaml \ --charts helm-charts/infisical-standalone-postgres \ --helm-extra-args="--timeout=300s" \ - --helm-extra-set-args="--set ingress.nginx.enabled=false --set infisical.autoDatabaseSchemaMigration=false --set infisical.replicaCount=1 --set infisical.image.tag=v0.132.2-postgres" \ + --helm-extra-set-args="--set ingress.nginx.enabled=false --set infisical.replicaCount=1 --set infisical.image.tag=v0.151.0" \ --namespace infisical-standalone-postgres release: diff --git a/.github/workflows/run-helm-chart-tests-infisical-standalone-postgres.yml b/.github/workflows/run-helm-chart-tests-infisical-standalone-postgres.yml index d48562fc6..f960aa6ff 100644 --- a/.github/workflows/run-helm-chart-tests-infisical-standalone-postgres.yml +++ b/.github/workflows/run-helm-chart-tests-infisical-standalone-postgres.yml @@ -66,5 +66,5 @@ jobs: --config ct.yaml \ --charts helm-charts/infisical-standalone-postgres \ --helm-extra-args="--timeout=300s" \ - --helm-extra-set-args="--set ingress.nginx.enabled=false --set infisical.autoDatabaseSchemaMigration=false --set infisical.replicaCount=1 --set infisical.image.tag=v0.132.2-postgres --set infisical.autoBootstrap.enabled=true" \ + --helm-extra-set-args="--set ingress.nginx.enabled=false --set infisical.replicaCount=1 --set infisical.image.tag=v0.151.0 --set infisical.autoBootstrap.enabled=true" \ --namespace infisical-standalone-postgres diff --git a/Dockerfile.fips.standalone-infisical b/Dockerfile.fips.standalone-infisical index 4dbf7872b..ab1d6fbb7 100644 --- a/Dockerfile.fips.standalone-infisical +++ b/Dockerfile.fips.standalone-infisical @@ -158,7 +158,7 @@ RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \ # Install Infisical CLI RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \ - && apt-get update && apt-get install -y infisical=0.42.6 \ + && apt-get update && apt-get install -y infisical=0.43.14 \ && rm -rf /var/lib/apt/lists/* RUN groupadd -r -g 1001 nodejs && useradd -r -u 1001 -g nodejs non-root-user diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index bc80130be..01c9a737b 100644 --- a/Dockerfile.standalone-infisical +++ b/Dockerfile.standalone-infisical @@ -142,7 +142,7 @@ RUN apt-get update && apt-get install -y \ # Install Infisical CLI RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash \ - && apt-get update && apt-get install -y infisical=0.42.6 \ + && apt-get update && apt-get install -y infisical=0.43.14 \ && rm -rf /var/lib/apt/lists/* WORKDIR / diff --git a/backend/Dockerfile b/backend/Dockerfile index fa3d0e509..a16d2b1fe 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -55,7 +55,7 @@ COPY --from=build /app . # Install Infisical CLI RUN apt-get install -y curl bash && \ curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \ - apt-get update && apt-get install -y infisical=0.41.89 git + apt-get update && apt-get install -y infisical=0.43.14 git HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \ CMD node healthcheck.js diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev index b5f4f7ac2..5e0612128 100644 --- a/backend/Dockerfile.dev +++ b/backend/Dockerfile.dev @@ -54,7 +54,7 @@ RUN apt-get install -y opensc # Install Infisical CLI RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \ apt-get update && \ - apt-get install -y infisical=0.41.89 + apt-get install -y infisical=0.43.14 WORKDIR /app diff --git a/backend/Dockerfile.dev.fips b/backend/Dockerfile.dev.fips index 4d5b84260..f30b059f9 100644 --- a/backend/Dockerfile.dev.fips +++ b/backend/Dockerfile.dev.fips @@ -67,7 +67,7 @@ RUN wget https://www.openssl.org/source/openssl-3.1.2.tar.gz \ # Install Infisical CLI RUN curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | bash && \ apt-get update && \ - apt-get install -y infisical=0.41.89 + apt-get install -y infisical=0.43.14 WORKDIR /app diff --git a/backend/src/db/migrations/20251023121055_fix-missing-group-memberships.ts b/backend/src/db/migrations/20251023121055_fix-missing-group-memberships.ts new file mode 100644 index 000000000..56fe82b2c --- /dev/null +++ b/backend/src/db/migrations/20251023121055_fix-missing-group-memberships.ts @@ -0,0 +1,60 @@ +import { Knex } from "knex"; + +import { AccessScope, TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasGroupsTable = await knex.schema.hasTable(TableName.Groups); + const hasMembershipTable = await knex.schema.hasTable(TableName.Membership); + const hasMembershipRoleTable = await knex.schema.hasTable(TableName.MembershipRole); + + if (!hasGroupsTable || !hasMembershipTable || !hasMembershipRoleTable) { + return; + } + + const groupsWithoutMembership = await knex + .select( + `${TableName.Groups}.id`, + `${TableName.Groups}.orgId`, + `${TableName.Groups}.role`, + `${TableName.Groups}.roleId` + ) + .from(TableName.Groups) + .leftJoin(TableName.Membership, `${TableName.Groups}.id`, `${TableName.Membership}.actorGroupId`) + .whereNull(`${TableName.Membership}.actorGroupId`); + + if (groupsWithoutMembership.length > 0) { + const membershipInserts = groupsWithoutMembership.map((group) => ({ + actorGroupId: group.id, + scope: AccessScope.Organization, + scopeOrgId: group.orgId, + isActive: true + })); + + const insertedMemberships = await knex(TableName.Membership).insert(membershipInserts).returning("*"); + + const membershipRoleInserts = insertedMemberships.map((membership, index) => { + const group = groupsWithoutMembership[index]; + return { + membershipId: membership.id, + role: group.role, + customRoleId: group.roleId + }; + }); + + await knex(TableName.MembershipRole).insert(membershipRoleInserts); + } + + await knex.schema.alterTable(TableName.Membership, (t) => { + t.check( + `("actorUserId" IS NOT NULL OR "actorIdentityId" IS NOT NULL OR "actorGroupId" IS NOT NULL)`, + undefined, + "at_least_one_actor" + ); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable(TableName.Membership, (t) => { + t.dropChecks("at_least_one_actor"); + }); +} diff --git a/backend/src/ee/routes/v1/pam-account-routers/index.ts b/backend/src/ee/routes/v1/pam-account-routers/index.ts index 568412c84..60d621467 100644 --- a/backend/src/ee/routes/v1/pam-account-routers/index.ts +++ b/backend/src/ee/routes/v1/pam-account-routers/index.ts @@ -1,3 +1,8 @@ +import { + CreateMySQLAccountSchema, + SanitizedMySQLAccountWithResourceSchema, + UpdateMySQLAccountSchema +} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas"; import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums"; import { CreatePostgresAccountSchema, @@ -16,5 +21,14 @@ export const PAM_ACCOUNT_REGISTER_ROUTER_MAP: Record { + registerPamResourceEndpoints({ + server, + resourceType: PamResource.MySQL, + accountResponseSchema: SanitizedMySQLAccountWithResourceSchema, + createAccountSchema: CreateMySQLAccountSchema, + updateAccountSchema: UpdateMySQLAccountSchema + }); } }; diff --git a/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts b/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts index 647f39d8d..d2e0183ff 100644 --- a/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts +++ b/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { PamFoldersSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { SanitizedMySQLAccountWithResourceSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas"; import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums"; import { SanitizedPostgresAccountWithResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; import { BadRequestError } from "@app/lib/errors"; @@ -10,8 +11,10 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -// Use z.union([...]) when more resources are added -const SanitizedAccountSchema = SanitizedPostgresAccountWithResourceSchema; +const SanitizedAccountSchema = z.union([ + SanitizedPostgresAccountWithResourceSchema, + SanitizedMySQLAccountWithResourceSchema +]); export const registerPamAccountRouter = async (server: FastifyZodProvider) => { server.route({ diff --git a/backend/src/ee/routes/v1/pam-resource-routers/index.ts b/backend/src/ee/routes/v1/pam-resource-routers/index.ts index 6b53781ae..c6c0afcca 100644 --- a/backend/src/ee/routes/v1/pam-resource-routers/index.ts +++ b/backend/src/ee/routes/v1/pam-resource-routers/index.ts @@ -4,6 +4,11 @@ import { SanitizedPostgresResourceSchema, UpdatePostgresResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; +import { + CreateMySQLResourceSchema, + MySQLResourceSchema, + UpdateMySQLResourceSchema +} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas"; import { registerPamResourceEndpoints } from "./pam-resource-endpoints"; @@ -16,5 +21,14 @@ export const PAM_RESOURCE_REGISTER_ROUTER_MAP: Record { + registerPamResourceEndpoints({ + server, + resourceType: PamResource.MySQL, + resourceResponseSchema: MySQLResourceSchema, + createResourceSchema: CreateMySQLResourceSchema, + updateResourceSchema: UpdateMySQLResourceSchema + }); } }; diff --git a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts index d42a73021..6563c86c7 100644 --- a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts +++ b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts @@ -1,6 +1,10 @@ import { z } from "zod"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { + MySQLResourceListItemSchema, + SanitizedMySQLResourceSchema +} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas"; import { PostgresResourceListItemSchema, SanitizedPostgresResourceSchema @@ -9,10 +13,12 @@ import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -// Use z.union([...]) when more resources are added -const SanitizedResourceSchema = SanitizedPostgresResourceSchema; +const SanitizedResourceSchema = z.union([SanitizedPostgresResourceSchema, SanitizedMySQLResourceSchema]); -const ResourceOptionsSchema = z.discriminatedUnion("resource", [PostgresResourceListItemSchema]); +const ResourceOptionsSchema = z.discriminatedUnion("resource", [ + PostgresResourceListItemSchema, + MySQLResourceListItemSchema +]); export const registerPamResourceRouter = async (server: FastifyZodProvider) => { server.route({ diff --git a/backend/src/ee/routes/v1/pam-session-router.ts b/backend/src/ee/routes/v1/pam-session-router.ts index c353fddfa..5fe10e434 100644 --- a/backend/src/ee/routes/v1/pam-session-router.ts +++ b/backend/src/ee/routes/v1/pam-session-router.ts @@ -2,14 +2,14 @@ import { z } from "zod"; import { PamSessionsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { MySQLSessionCredentialsSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas"; import { PostgresSessionCredentialsSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; import { PamSessionCommandLogSchema, SanitizedSessionSchema } from "@app/ee/services/pam-session/pam-session-schemas"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -// Use z.union([]) once there's multiple -const SessionCredentialsSchema = PostgresSessionCredentialsSchema; +const SessionCredentialsSchema = z.union([PostgresSessionCredentialsSchema, MySQLSessionCredentialsSchema]); export const registerPamSessionRouter = async (server: FastifyZodProvider) => { // Meant to be hit solely by gateway identities diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index bb56d4df4..bbd6147ed 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -40,6 +40,7 @@ import { TOrgPlanDTO, TOrgPlansTableDTO, TOrgPmtMethodsDTO, + TPlanBillingInfo, TStartOrgTrialDTO, TUpdateOrgBillingDetailsDTO } from "./license-types"; @@ -465,6 +466,21 @@ export const licenseServiceFactory = ({ return { url }; }; + const getUsageMetrics = async (orgId: string) => { + const [orgMembersUsed, identityUsed, projectCount] = await Promise.all([ + orgDAL.countAllOrgMembers(orgId), + licenseDAL.countOfOrgIdentities(orgId), + projectDAL.countOfOrgProjects(orgId) + ]); + + return { + orgMembersUsed, + identityUsed, + projectCount, + totalIdentities: identityUsed + orgMembersUsed + }; + }; + const getOrgBillingInfo = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { const { permission } = await permissionService.getOrgPermission({ actorId, @@ -483,10 +499,16 @@ export const licenseServiceFactory = ({ }); } if (instanceType === InstanceType.Cloud) { - const { data } = await licenseServerCloudApi.request.get( + const { data } = await licenseServerCloudApi.request.get( `/api/license-server/v1/customers/${organization.customerId}/cloud-plan/billing` ); - return data; + const { identityUsed, orgMembersUsed } = await getUsageMetrics(orgId); + + return { + ...data, + users: orgMembersUsed, + identities: identityUsed + }; } return { @@ -495,7 +517,9 @@ export const licenseServiceFactory = ({ interval: "month", intervalCount: 1, amount: 0, - quantity: 1 + quantity: 1, + users: 0, + identities: 0 }; }; @@ -539,21 +563,6 @@ export const licenseServiceFactory = ({ throw new Error(`Unsupported instance type for server-based plan table: ${instanceType}`); }; - const getUsageMetrics = async (orgId: string) => { - const [orgMembersUsed, identityUsed, projectCount] = await Promise.all([ - orgDAL.countAllOrgMembers(orgId), - licenseDAL.countOfOrgIdentities(orgId), - projectDAL.countOfOrgProjects(orgId) - ]); - - return { - orgMembersUsed, - identityUsed, - projectCount, - totalIdentities: identityUsed + orgMembersUsed - }; - }; - // returns org current plan feature table const getOrgPlanTable = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { const { permission } = await permissionService.getOrgPermission({ diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 93f40ae6e..c4ff6a8fa 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -22,6 +22,15 @@ export type TOfflineLicense = { features: TFeatureSet; }; +export type TPlanBillingInfo = { + currentPeriodStart: number; + currentPeriodEnd: number; + interval: "month" | "year"; + intervalCount: number; + amount: number; + quantity: number; +}; + export type TFeatureSet = { _id: null; slug: string | null; diff --git a/backend/src/ee/services/pam-resource/mysql/mysql-resource-fns.ts b/backend/src/ee/services/pam-resource/mysql/mysql-resource-fns.ts new file mode 100644 index 000000000..4010d208b --- /dev/null +++ b/backend/src/ee/services/pam-resource/mysql/mysql-resource-fns.ts @@ -0,0 +1,8 @@ +import { MySQLResourceListItemSchema } from "./mysql-resource-schemas"; + +export const getMySQLResourceListItem = () => { + return { + name: MySQLResourceListItemSchema.shape.name.value, + resource: MySQLResourceListItemSchema.shape.resource.value + }; +}; diff --git a/backend/src/ee/services/pam-resource/mysql/mysql-resource-schemas.ts b/backend/src/ee/services/pam-resource/mysql/mysql-resource-schemas.ts new file mode 100644 index 000000000..8d3589a8a --- /dev/null +++ b/backend/src/ee/services/pam-resource/mysql/mysql-resource-schemas.ts @@ -0,0 +1,76 @@ +import { z } from "zod"; + +import { PamResource } from "../pam-resource-enums"; +import { + BaseCreatePamAccountSchema, + BaseCreatePamResourceSchema, + BasePamAccountSchema, + BasePamAccountSchemaWithResource, + BasePamResourceSchema, + BaseUpdatePamAccountSchema, + BaseUpdatePamResourceSchema +} from "../pam-resource-schemas"; +import { + BaseSqlAccountCredentialsSchema, + BaseSqlResourceConnectionDetailsSchema +} from "../shared/sql/sql-resource-schemas"; + +// Resources +export const MySQLResourceConnectionDetailsSchema = BaseSqlResourceConnectionDetailsSchema.extend({ + // MySQL db in many cases the db will not be provided when making connection + database: z.string().trim() +}); +export const MySQLAccountCredentialsSchema = BaseSqlAccountCredentialsSchema; + +const BaseMySQLResourceSchema = BasePamResourceSchema.extend({ resourceType: z.literal(PamResource.MySQL) }); + +export const MySQLResourceSchema = BaseMySQLResourceSchema.extend({ + connectionDetails: MySQLResourceConnectionDetailsSchema, + rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional() +}); + +export const SanitizedMySQLResourceSchema = BaseMySQLResourceSchema.extend({ + connectionDetails: MySQLResourceConnectionDetailsSchema, + rotationAccountCredentials: MySQLAccountCredentialsSchema.pick({ + username: true + }) + .nullable() + .optional() +}); + +export const MySQLResourceListItemSchema = z.object({ + name: z.literal("MySQL"), + resource: z.literal(PamResource.MySQL) +}); + +export const CreateMySQLResourceSchema = BaseCreatePamResourceSchema.extend({ + connectionDetails: MySQLResourceConnectionDetailsSchema, + rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional() +}); + +export const UpdateMySQLResourceSchema = BaseUpdatePamResourceSchema.extend({ + connectionDetails: MySQLResourceConnectionDetailsSchema.optional(), + rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional() +}); + +// Accounts +export const MySQLAccountSchema = BasePamAccountSchema.extend({ + credentials: MySQLAccountCredentialsSchema +}); + +export const CreateMySQLAccountSchema = BaseCreatePamAccountSchema.extend({ + credentials: MySQLAccountCredentialsSchema +}); + +export const UpdateMySQLAccountSchema = BaseUpdatePamAccountSchema.extend({ + credentials: MySQLAccountCredentialsSchema.optional() +}); + +export const SanitizedMySQLAccountWithResourceSchema = BasePamAccountSchemaWithResource.extend({ + credentials: MySQLAccountCredentialsSchema.pick({ + username: true + }) +}); + +// Sessions +export const MySQLSessionCredentialsSchema = MySQLResourceConnectionDetailsSchema.and(MySQLAccountCredentialsSchema); diff --git a/backend/src/ee/services/pam-resource/mysql/mysql-resource-types.ts b/backend/src/ee/services/pam-resource/mysql/mysql-resource-types.ts new file mode 100644 index 000000000..43c2eea51 --- /dev/null +++ b/backend/src/ee/services/pam-resource/mysql/mysql-resource-types.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; + +import { + MySQLAccountCredentialsSchema, + MySQLAccountSchema, + MySQLResourceConnectionDetailsSchema, + MySQLResourceSchema +} from "./mysql-resource-schemas"; + +// Resources +export type TMySQLResource = z.infer; +export type TMySQLResourceConnectionDetails = z.infer; + +// Accounts +export type TMySQLAccount = z.infer; +export type TMySQLAccountCredentials = z.infer; diff --git a/backend/src/ee/services/pam-resource/pam-resource-enums.ts b/backend/src/ee/services/pam-resource/pam-resource-enums.ts index fbc260fba..dff1cc650 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-enums.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-enums.ts @@ -1,3 +1,4 @@ export enum PamResource { - Postgres = "postgres" + Postgres = "postgres", + MySQL = "mysql" } diff --git a/backend/src/ee/services/pam-resource/pam-resource-factory.ts b/backend/src/ee/services/pam-resource/pam-resource-factory.ts index 298b1664c..151fa7ea1 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-factory.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-factory.ts @@ -5,5 +5,6 @@ import { sqlResourceFactory } from "./shared/sql/sql-resource-factory"; type TPamResourceFactoryImplementation = TPamResourceFactory; export const PAM_RESOURCE_FACTORY_MAP: Record = { - [PamResource.Postgres]: sqlResourceFactory as TPamResourceFactoryImplementation + [PamResource.Postgres]: sqlResourceFactory as TPamResourceFactoryImplementation, + [PamResource.MySQL]: sqlResourceFactory as TPamResourceFactoryImplementation }; diff --git a/backend/src/ee/services/pam-resource/pam-resource-fns.ts b/backend/src/ee/services/pam-resource/pam-resource-fns.ts index 9d7493e68..cad087d2f 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-fns.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-fns.ts @@ -3,11 +3,12 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { decryptAccountCredentials } from "../pam-account/pam-account-fns"; +import { getMySQLResourceListItem } from "./mysql/mysql-resource-fns"; import { TPamResource, TPamResourceConnectionDetails } from "./pam-resource-types"; import { getPostgresResourceListItem } from "./postgres/postgres-resource-fns"; export const listResourceOptions = () => { - return [getPostgresResourceListItem()].sort((a, b) => a.name.localeCompare(b.name)); + return [getPostgresResourceListItem(), getMySQLResourceListItem()].sort((a, b) => a.name.localeCompare(b.name)); }; // Resource diff --git a/backend/src/ee/services/pam-resource/pam-resource-types.ts b/backend/src/ee/services/pam-resource/pam-resource-types.ts index f2016420a..1ca9db3e2 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-types.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-types.ts @@ -1,4 +1,10 @@ import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service"; +import { + TMySQLAccount, + TMySQLAccountCredentials, + TMySQLResource, + TMySQLResourceConnectionDetails +} from "./mysql/mysql-resource-types"; import { PamResource } from "./pam-resource-enums"; import { TPostgresAccount, @@ -8,12 +14,13 @@ import { } from "./postgres/postgres-resource-types"; // Resource types -export type TPamResource = TPostgresResource; -export type TPamResourceConnectionDetails = TPostgresResourceConnectionDetails; +export type TPamResource = TPostgresResource | TMySQLResource; +export type TPamResourceConnectionDetails = TPostgresResourceConnectionDetails | TMySQLResourceConnectionDetails; // Account types -export type TPamAccount = TPostgresAccount; -export type TPamAccountCredentials = TPostgresAccountCredentials; +export type TPamAccount = TPostgresAccount | TMySQLAccount; +// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents +export type TPamAccountCredentials = TPostgresAccountCredentials | TMySQLAccountCredentials; // Resource DTOs export type TCreateResourceDTO = Pick< diff --git a/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts index 73defd6e6..a171b39b5 100644 --- a/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts +++ b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts @@ -1,4 +1,6 @@ -import knex, { Knex } from "knex"; +import knex from "knex"; +import mysql, { Connection } from "mysql2/promise"; +import * as pg from "pg"; import tls, { PeerCertificate } from "tls"; import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns"; @@ -20,30 +22,160 @@ const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000; const TEST_CONNECTION_USERNAME = "infisical-gateway-connection-test"; const TEST_CONNECTION_PASSWORD = "infisical-gateway-connection-test-password"; +const SIMPLE_QUERY = "select 1"; -const SQL_CONNECTION_CLIENT_MAP = { - [PamResource.Postgres]: "pg" -}; +export interface SqlResourceConnection { + /** + * Check and see if the connection is good or not. + * + * @param connectOnly when true, if we only want to know that making the connection is possible or not, + * we don't care about authentication failures + * @returns Promise to be resolved when the connection is good, otherwise an error will be errbacked + */ + validate: (connectOnly: boolean) => Promise; -const getConnectionConfig = ( - resourceType: PamResource, - { host, sslEnabled, sslRejectUnauthorized, sslCertificate }: TSqlResourceConnectionDetails -) => { - switch (resourceType) { + /** + * Rotate password and return the new credentials. + * + * @param currentCredentials the current credentials to rotate + * + * @returns Promise to be resolved with the new credentials + */ + rotateCredentials: (currentCredentials: TSqlAccountCredentials) => Promise; + + /** + * Close the connection. + * + * @returns Promise for closing the connection + */ + close: () => Promise; +} + +const makeSqlConnection = ( + proxyPort: number, + config: { + connectionDetails: TSqlResourceConnectionDetails; + resourceType: PamResource; + username?: string; + password?: string; + } +): SqlResourceConnection => { + const { connectionDetails, resourceType, username, password } = config; + const { host, sslEnabled, sslRejectUnauthorized, sslCertificate } = connectionDetails; + const actualUsername = username ?? TEST_CONNECTION_USERNAME; // Use provided username or fallback + const actualPassword = password ?? TEST_CONNECTION_PASSWORD; // Use provided password or fallback + switch (config.resourceType) { case PamResource.Postgres: { + const client = knex({ + client: "pg", + connection: { + host: "localhost", + port: proxyPort, + user: actualUsername, + password: actualPassword, + database: connectionDetails.database, + connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT, + ssl: sslEnabled + ? { + rejectUnauthorized: sslRejectUnauthorized, + ca: sslCertificate, + servername: host, + // When using proxy, we need to bypass hostname validation since we connect to localhost + // but validate the certificate against the actual hostname + checkServerIdentity: (hostname: string, cert: PeerCertificate) => { + return tls.checkServerIdentity(host, cert); + } + } + : false + } + }); return { - ssl: sslEnabled - ? { - rejectUnauthorized: sslRejectUnauthorized, - ca: sslCertificate, - servername: host, - // When using proxy, we need to bypass hostname validation since we connect to localhost - // but validate the certificate against the actual hostname - checkServerIdentity: (hostname: string, cert: PeerCertificate) => { - return tls.checkServerIdentity(host, cert); + validate: async (connectOnly) => { + try { + await client.raw(SIMPLE_QUERY); + } catch (error) { + if (error instanceof pg.DatabaseError) { + // Hacky way to know if we successfully hit the database. + // TODO: potentially two approaches to solve the problem. + // 1. change the work flow, add account first then resource + // 2. modify relay to add a new endpoint for returning if the target host is healthy or not + // (like being able to do an auth handshake regardless pass or not) + if ( + connectOnly && + (error.message === `password authentication failed for user "${TEST_CONNECTION_USERNAME}"` || + error.message.includes("no pg_hba.conf entry for host")) + ) { + return; } } - : false + throw new BadRequestError({ + message: `Unable to validate connection to ${resourceType}: ${(error as Error).message || String(error)}` + }); + } + }, + rotateCredentials: async (currentCredentials) => { + const newPassword = alphaNumericNanoId(32); + // Note: The generated random password is not really going to make SQL Injection possible. + // The reason we are not using parameters binding is that the "ALTER USER" syntax is DDL, + // parameters binding is not supported. But just in case if the this code got copied + // around and repurposed, let's just do some naive escaping regardless + await client.raw(`ALTER USER :username: WITH PASSWORD '${newPassword.replace(/'/g, "''")}'`, { + username: currentCredentials.username + }); + return { username: currentCredentials.username, password: newPassword }; + }, + close: () => client.destroy() + }; + } + case PamResource.MySQL: { + return { + validate: async (connectOnly) => { + let client: Connection | null = null; + try { + // Notice: the reason we are not using Knex for mysql2 is because we don't need any fancy feature from Knex. + // mysql2 doesn't provide custom ssl verification function pass in. + // ref: https://github.com/sidorares/node-mysql2/blob/2543272a2ada8d8a07f74582549d7dd3fe948e2d/lib/base/connection.js#L358-L362 + // and then even I tried to workaround it with Knex's pool afterCreate hook, but then encounter a bug: + // ref: https://github.com/knex/knex/issues/5352 + // It appears that using Knex causing more troubles than not, we are just checking the connections, + // so it's much easier to create raw connection with the driver lib directly + client = await mysql.createConnection({ + host: "localhost", + port: proxyPort, + user: actualUsername, // Use provided username or fallback + password: actualPassword, // Use provided password or fallback + database: connectionDetails.database, + ssl: sslEnabled + ? { + rejectUnauthorized: sslRejectUnauthorized, + ca: sslCertificate + } + : undefined + }); + await client.query(SIMPLE_QUERY); + } catch (error) { + if (connectOnly) { + // Hacky way to know if we successfully hit the database. + if ( + error instanceof Error && + error.message.startsWith(`Access denied for user '${TEST_CONNECTION_USERNAME}'@`) + ) { + return; + } + } + // TODO: handle other errors, and throw standardlized errors providing user-friendly msg + throw error; + } finally { + await client?.end(); + } + }, + rotateCredentials: async () => { + // TODO: the pwd rotation for MySQL is not supported yet + throw new BadRequestError({ + message: "Unsupported operation" + }); + }, + close: async () => {} }; } default: @@ -62,10 +194,9 @@ export const executeWithGateway = async ( password?: string; }, gatewayV2Service: Pick, - operation: (client: Knex) => Promise + operation: (connection: SqlResourceConnection) => Promise ): Promise => { - const { connectionDetails, resourceType, gatewayId, username, password } = config; - + const { connectionDetails, gatewayId } = config; const [targetHost] = await verifyHostInputValidity(connectionDetails.host, true); const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({ gatewayId, @@ -79,22 +210,11 @@ export const executeWithGateway = async ( return withGatewayV2Proxy( async (proxyPort) => { - const client = knex({ - client: SQL_CONNECTION_CLIENT_MAP[resourceType], - connection: { - database: connectionDetails.database, - port: proxyPort, - host: "localhost", - user: username ?? TEST_CONNECTION_USERNAME, // Use provided username or fallback - password: password ?? TEST_CONNECTION_PASSWORD, // Use provided password or fallback - connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT, - ...getConnectionConfig(resourceType, connectionDetails) - } - }); + const connection = makeSqlConnection(proxyPort, config); try { - return await operation(client); + return await operation(connection); } finally { - await client.destroy(); + await connection.close(); } }, { @@ -115,25 +235,14 @@ export const sqlResourceFactory: TPamResourceFactory { try { await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (client) => { - await client.raw("Select 1"); + await client.validate(true); }); return connectionDetails; } catch (error) { - // Hacky way to know if we successfully hit the database - if (error instanceof BadRequestError) { - if (error.message === `password authentication failed for user "${TEST_CONNECTION_USERNAME}"`) { - return connectionDetails; - } - - if (error.message.includes("no pg_hba.conf entry for host")) { - return connectionDetails; - } - - if (error.message === "Connection terminated unexpectedly") { - throw new BadRequestError({ - message: "Connection terminated unexpectedly. Verify that host and port are correct" - }); - } + if (error instanceof BadRequestError && error.message === "Connection terminated unexpectedly") { + throw new BadRequestError({ + message: "Connection terminated unexpectedly. Verify that host and port are correct" + }); } throw new BadRequestError({ @@ -156,11 +265,12 @@ export const sqlResourceFactory: TPamResourceFactory { - await client.raw("Select 1"); + await client.validate(false); } ); return credentials; } catch (error) { + // TODO: extract these logic into each SQL connection if (error instanceof BadRequestError) { if (error.message === `password authentication failed for user "${credentials.username}"`) { throw new BadRequestError({ @@ -186,9 +296,7 @@ export const sqlResourceFactory: TPamResourceFactory { try { - const newPassword = alphaNumericNanoId(32); - - await executeWithGateway( + return await executeWithGateway( { connectionDetails, gatewayId, @@ -197,20 +305,8 @@ export const sqlResourceFactory: TPamResourceFactory { - switch (resourceType) { - case PamResource.Postgres: - await client.raw(`ALTER USER ?? WITH PASSWORD '${newPassword}'`, [currentCredentials.username]); - break; - default: - throw new BadRequestError({ - message: `Password rotation for ${resourceType as PamResource} is not supported.` - }); - } - } + (client) => client.rotateCredentials(currentCredentials) ); - - return { username: currentCredentials.username, password: newPassword }; } catch (error) { if (error instanceof BadRequestError) { if (error.message === `password authentication failed for user "${rotationAccountCredentials.username}"`) { diff --git a/backend/src/ee/services/pam-resource/shared/sql/sql-resource-types.ts b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-types.ts index f56a2a3dc..a9ff49013 100644 --- a/backend/src/ee/services/pam-resource/shared/sql/sql-resource-types.ts +++ b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-types.ts @@ -1,7 +1,9 @@ +import { TMySQLAccountCredentials, TMySQLResourceConnectionDetails } from "../../mysql/mysql-resource-types"; import { TPostgresAccountCredentials, TPostgresResourceConnectionDetails } from "../../postgres/postgres-resource-types"; -export type TSqlResourceConnectionDetails = TPostgresResourceConnectionDetails; -export type TSqlAccountCredentials = TPostgresAccountCredentials; +export type TSqlResourceConnectionDetails = TPostgresResourceConnectionDetails | TMySQLResourceConnectionDetails; +// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents +export type TSqlAccountCredentials = TPostgresAccountCredentials | TMySQLAccountCredentials; diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index 13b862343..c99ae8b28 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -84,7 +84,7 @@ type TSamlConfigServiceFactoryDep = { projectDAL: Pick; projectBotDAL: Pick; projectKeyDAL: Pick; - membershipGroupDAL: Pick; + membershipGroupDAL: Pick; }; export const samlConfigServiceFactory = ({ @@ -183,6 +183,22 @@ export const samlConfigServiceFactory = ({ transaction ); orgGroupsMap.set(groupName, newGroup); + const orgMembership = await membershipGroupDAL.create( + { + actorGroupId: newGroup.id, + scope: AccessScope.Organization, + scopeOrgId: orgId + }, + transaction + ); + await membershipRoleDAL.create( + { + membershipId: orgMembership.id, + role: OrgMembershipRole.NoAccess, + customRoleId: null + }, + transaction + ); } } diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 2da7a245a..9fc4cff92 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -3,13 +3,13 @@ import { z } from "zod"; import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; import { crypto } from "@app/lib/crypto/cryptography"; import { QueueWorkerProfile } from "@app/lib/types"; +import { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; import { TSuperAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { BadRequestError } from "../errors"; import { removeTrailingSlash } from "../fn"; import { CustomLogger } from "../logger/logger"; import { zpStr } from "../zod"; -import { TKmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal"; export const GITLAB_URL = "https://gitlab.com"; diff --git a/backend/src/server/lib/schemas.ts b/backend/src/server/lib/schemas.ts index d0750926d..035e645f4 100644 --- a/backend/src/server/lib/schemas.ts +++ b/backend/src/server/lib/schemas.ts @@ -43,6 +43,6 @@ export const GenericResourceNameSchema = z export const BaseSecretNameSchema = z.string().trim().min(1); export const SecretNameSchema = BaseSecretNameSchema.refine( - (el) => !el.includes(":"), - "Secret name cannot contain colon." -).refine((el) => !el.includes("/"), "Secret name cannot contain forward slash."); + (el) => !el.includes(":") && !el.includes("/"), + "Secret name cannot contain colon or forward slash." +); diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-fns.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-fns.ts index c8b494e7b..a028bc25f 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-fns.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-fns.ts @@ -8,7 +8,7 @@ interface PrincipalArnEntity { SessionInfo: string; // Only populated for assumed-role } -export const extractPrincipalArnEntity = (arn: string): PrincipalArnEntity => { +export const extractPrincipalArnEntity = (arn: string, formatAsIamRole: boolean = false): PrincipalArnEntity => { // split the ARN into parts using ":" as the delimiter const fullParts = arn.split(":"); if (fullParts.length !== 6) { @@ -49,7 +49,7 @@ export const extractPrincipalArnEntity = (arn: string): PrincipalArnEntity => { } // assumed roles use a special format where the friendly name is the role name const [roleName, sessionId] = rest; - finalType = "assumed-role"; + finalType = formatAsIamRole ? "role" : "assumed-role"; friendlyName = roleName; sessionInfo = sessionId; break; @@ -83,9 +83,11 @@ export const extractPrincipalArnEntity = (arn: string): PrincipalArnEntity => { * Extracts the identity ARN from the GetCallerIdentity response to one of the following formats: * - arn:aws:iam::123456789012:user/MyUserName * - arn:aws:iam::123456789012:role/MyRoleName + * - arn:aws-us-gov:iam::123456789012:user/MyUserName (GovCloud) + * - arn:aws-us-gov:iam::123456789012:role/MyRoleName (GovCloud) */ -export const extractPrincipalArn = (arn: string) => { - const entity = extractPrincipalArnEntity(arn); +export const extractPrincipalArn = (arn: string, formatAsIamRole: boolean = false) => { + const entity = extractPrincipalArnEntity(arn, formatAsIamRole); - return `arn:aws:${entity.Service}::${entity.AccountNumber}:${entity.Type}/${entity.FriendlyName}`; + return `arn:${entity.Partition}:${formatAsIamRole ? "iam" : entity.Service}::${entity.AccountNumber}:${entity.Type}/${entity.FriendlyName}`; }; diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts index 3bbe88824..e61e8296b 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts @@ -158,7 +158,7 @@ export const identityAwsAuthServiceFactory = ({ // considers exact matches + wildcard matches // heavily validated in router const regex = new RE2(`^${principalArn.replaceAll("*", ".*")}$`); - return regex.test(formattedArn); + return regex.test(formattedArn) || regex.test(extractPrincipalArn(Arn, true)); }); if (!isArnAllowed) { diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts index 4e3884e15..ed7f6696a 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-validators.ts @@ -6,7 +6,7 @@ const twelveDigitRegex = new RE2(/^\d{12}$/); // akhilmhdh: change this to a normal function later. Checked no redosable at the moment const arnRegex = new RE2( - /^arn:aws:(iam|sts)::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/[a-zA-Z0-9_.@+*/-]+|assumed-role\/[a-zA-Z0-9_.@+*/-]+|\*)$/ + /^arn:aws(?:-us-gov)?:(iam|sts)::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/[a-zA-Z0-9_.@+*/-]+|assumed-role\/[a-zA-Z0-9_.@+*/-]+|\*)$/ ); export const validateAccountIds = z @@ -55,7 +55,7 @@ export const validatePrincipalArns = z }, { message: - "Each ARN must be in the format of 'arn:aws:iam::123456789012:user/UserName', 'arn:aws:iam::123456789012:role/RoleName', or 'arn:aws:iam::123456789012:*', 'arn:aws:sts::123456789012:assumed-role/RoleName'." + "Each ARN must be in the format of 'arn:aws:iam::123456789012:user/UserName', 'arn:aws:iam::123456789012:role/RoleName', or 'arn:aws:iam::123456789012:*', 'arn:aws:sts::123456789012:assumed-role/RoleName'. GovCloud ARNs (arn:aws-us-gov:...) are also supported." } ) // Transform to normalize the spaces around commas diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index 63faf3b03..e83a0033f 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -11,6 +11,7 @@ import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; +import { SecretNameSchema } from "@app/server/lib/schemas"; import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; import { ActorType } from "@app/services/auth/auth-type"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; @@ -408,6 +409,24 @@ export const secretSyncQueueFactory = ({ if (!Object.keys(importedSecrets).length) return {}; + let invalidNameCount = 0; + let errorMessage = ""; + + for (const [key] of Object.entries(importedSecrets)) { + const result = SecretNameSchema.safeParse(key); + if (!result.success) { + invalidNameCount += 1; + if (errorMessage === "") errorMessage = result.error.issues[0]?.message; + } + } + + if (invalidNameCount > 0) { + throw new SecretSyncError({ + message: `Found ${invalidNameCount} invalid secret name${invalidNameCount === 1 ? "" : "s"}. ${errorMessage}`, + shouldRetry: false + }); + } + const importedSecretMap: TSecretMap = {}; const secretMap = await $getInfisicalSecrets(secretSync, false); diff --git a/docs/cli/commands/login.mdx b/docs/cli/commands/login.mdx index 1f7a08350..c58c13713 100644 --- a/docs/cli/commands/login.mdx +++ b/docs/cli/commands/login.mdx @@ -49,10 +49,13 @@ User authentication is designed for individual developers and supports multiple - Your email address. Required for direct login along with `--password`. + Your email address. Required for direct login along with `--password` and `--organization-id`. - Your password. Required for direct login along with `--email`. + Your password. Required for direct login along with `--email` and `--organization-id`. + + + Your organization id. Required for direct login along with `--password` and `--email`. Force interactive CLI login instead of browser-based authentication. @@ -71,11 +74,12 @@ User authentication is designed for individual developers and supports multiple ```bash - infisical login --email=user@example.com --password=your-password + infisical login --email=user@example.com --password=your-password --organization-id=your-organization-id # Or using environment variables export INFISICAL_EMAIL="user@example.com" export INFISICAL_PASSWORD="your-password" + export INFISICAL_ORGANIZATION_ID="your-organization-id" infisical login ``` @@ -86,7 +90,7 @@ User authentication is designed for individual developers and supports multiple ```bash - export INFISICAL_TOKEN=$(infisical login --email=user@example.com --password=your-password --plain --silent) + export INFISICAL_TOKEN=$(infisical login --email=user@example.com --password=your-password --organization-id=your-organization-id --plain --silent) ``` @@ -404,11 +408,11 @@ The login command supports a number of flags that you can use for different auth ```bash - infisical login --email= --password= + infisical login --email= --password= --organization-id= ``` #### Description - User email address. Required if you want to do a non-interactive login when the **--method** flag is set to **user**. Must be used together with the `--password` flag. + User email address. Required if you want to do a non-interactive login when the **--method** flag is set to **user**. Must be used together with the `--password` and `--organization-id` flag. You can omit the **--method=user** if you want as it's the default method. @@ -421,11 +425,11 @@ The login command supports a number of flags that you can use for different auth ```bash - infisical login --email= --password= + infisical login --email= --password= --organization-id= ``` #### Description - User password. Required if you want to do a non-interactive login when the **--method** flag is set to **user**. Must be used together with the `--email` flag. + User password. Required if you want to do a non-interactive login when the **--method** flag is set to **user**. Must be used together with the `--email` and `--organization-id` flag. For security in CI/CD environments, prefer using the `INFISICAL_PASSWORD` environment variable instead of passing the password as a command-line flag. @@ -439,6 +443,23 @@ The login command supports a number of flags that you can use for different auth The `password` flag can be substituted with the `INFISICAL_PASSWORD` environment variable. + + + ```bash + infisical login --email= --password= --organization-id= + ``` + + #### Description + User organization id. Required if you want to do a non-interactive login when the **--method** flag is set to **user**. Must be used together with the `--email` and `--password` flag. + + + You can omit the **--method=user** if you want as it's the default method. + + + + The `organization-id` flag can be substituted with the `INFISICAL_ORGANIZATION_ID` environment variable. + + ```bash @@ -446,12 +467,12 @@ The login command supports a number of flags that you can use for different auth ``` #### Description - Forces interactive CLI login where you'll be prompted to enter your email and password in the terminal, instead of opening a browser. + Forces interactive CLI login where you'll be prompted to enter your email, password, and select your organization in the terminal, instead of opening a browser. ```bash - infisical login --email= --password= --plain + infisical login --email= --password= --organization-id= --plain ``` #### Description @@ -459,7 +480,7 @@ The login command supports a number of flags that you can use for different auth ```bash # Example: Capture token in a variable - export INFISICAL_TOKEN=$(infisical login --email= --password= --plain --silent) + export INFISICAL_TOKEN=$(infisical login --email= --password= --organization-id= --plain --silent) ``` @@ -506,13 +527,13 @@ The following examples demonstrate different ways to authenticate as a user with ```bash # Basic direct login (defaults to US Cloud) - infisical login --email user@example.com --password "your-password" + infisical login --email user@example.com --password "your-password" --organization-id "your-organization-id" # EU Cloud (Custom domain) - infisical login --email user@example.com --password "your-password" --domain https://eu.infisical.com + infisical login --email user@example.com --password "your-password" --organization-id "your-organization-id" --domain https://eu.infisical.com # Output only JWT token for scripting - export INFISICAL_TOKEN=$(infisical login --email user@example.com --password "your-password" --plain --silent) + export INFISICAL_TOKEN=$(infisical login --email user@example.com --password "your-password" --organization-id "your-organization-id" --plain --silent) ``` #### Using Environment Variables (Recommended for CI/CD) @@ -521,6 +542,7 @@ The following examples demonstrate different ways to authenticate as a user with # Set credentials as environment variables export INFISICAL_EMAIL="user@example.com" export INFISICAL_PASSWORD="your-password" + export INFISICAL_ORGANIZATION_ID="your-organization-id" # Login without additional flags infisical login @@ -542,6 +564,8 @@ The following examples demonstrate different ways to authenticate as a user with - Email address - Password + After the prompt, you will be shown a list of organizations to choose from. + diff --git a/docs/documentation/platform/kms/hsm-integration.mdx b/docs/documentation/platform/kms/hsm-integration.mdx index 45d883977..7a8d15fe5 100644 --- a/docs/documentation/platform/kms/hsm-integration.mdx +++ b/docs/documentation/platform/kms/hsm-integration.mdx @@ -1428,7 +1428,7 @@ Enabling HSM encryption has a set of key benefits: infisical: image: repository: infisical/infisical - tag: "v0.151.0-nightly-20251013.1" + tag: "v0.151.0" pullPolicy: IfNotPresent extraVolumeMounts: diff --git a/docs/documentation/platform/kms/kmip.mdx b/docs/documentation/platform/kms/kmip.mdx index 1e025d51a..d82269447 100644 --- a/docs/documentation/platform/kms/kmip.mdx +++ b/docs/documentation/platform/kms/kmip.mdx @@ -8,135 +8,256 @@ description: "Learn more about integrating with Infisical KMS using KMIP (Key Ma sales@infisical.com if you have any questions. -## Overview +Infisical KMS provides Key Management Interoperability Protocol (KMIP) support for integration with KMIP-compatible clients. This allows for enhanced key management across various applications that support the KMIP 1.4 protocol. -Infisical KMS provides **Key Management Interoperability Protocol (KMIP)** support, enabling seamless integration with KMIP-compatible clients. This allows for enhanced key management across various applications that support the **KMIP 1.4 protocol**. +## How KMIP Works with Infisical -## Supported Operations +At a high level, the KMIP integration follows this architecture: -The Infisical KMIP server supports the following operations for **symmetric keys**: -- **Create** - Generate symmetric keys. -- **Register** - Register externally created keys. -- **Locate** - Find keys using attributes. -- **Get** - Retrieve keys securely. -- **Activate** - Enable keys for usage. -- **Revoke** - Revoke existing keys. -- **Destroy** - Permanently remove keys. -- **Get Attributes** - Retrieve metadata associated with keys. -- **Query** - Query server capabilities and supported operations. +![KMIP Architecture Diagram](/images/kmip-diagram.png) -## Benefits of KMIP Integration +At a high level, the KMIP integration works as follows: -Integrating Infisical KMS with KMIP-compatible clients provides the following benefits: +1. KMIP clients (your applications or tools) communicate with the KMIP server +2. The KMIP server acts as a proxy and forwards requests to Infisical KMS +3. The KMIP server authenticates to Infisical using a machine identity -- **Standardized Key Management**: Allows interoperability with security and cryptographic applications that support KMIP. -- **Enterprise-Grade Security**: Utilizes Infisical’s encryption mechanisms to securely store and manage keys. -- **Centralized Key Management**: Enables a unified approach for managing cryptographic keys across multiple environments. +The KMIP server itself is deployed using the Infisical CLI (`infisical kmip start` command) and serves as an intermediary between your KMIP clients and Infisical's key management system. -## Compatibility +### Supported Operations -Infisical KMIP supports **KMIP versions 1.0 to 1.4**, ensuring compatibility with a wide range of clients and security tools. +The Infisical KMIP server supports the following operations for symmetric keys: -## Secure Communication & Authorization +- **Create** - Generate symmetric keys +- **Register** - Register externally created keys +- **Locate** - Find keys using attributes +- **Get** - Retrieve keys securely +- **Activate** - Enable keys for usage +- **Revoke** - Revoke existing keys +- **Destroy** - Permanently remove keys +- **Get Attributes** - Retrieve metadata associated with keys +- **Query** - Query server capabilities and supported operations -KMIP client-server communication is secured using **mutual TLS (mTLS)**, ensuring strong identity verification and encrypted data exchange via **PKI certificates**. Each KMIP entity must possess valid certificates signed by a trusted Root CA to establish trust. -For strong isolation, each Infisical organization has its own KMIP PKI (Public Key Infrastructure), ensuring that cryptographic operations and certificate authorities remain separate across organizations. +### Compatibility -Infisical KMS enforces a **two-layer authorization model** for KMIP operations: +Infisical KMIP supports KMIP versions 1.0 to 1.4, ensuring compatibility with a wide range of clients and security tools. -1. **KMIP Server Authorization** – The KMIP server, acting as a proxy, must have the `proxy KMIP` permission to forward client requests to Infisical KMS. This is done using a **machine identity** attached to the KMIP server. -2. **KMIP Client Authorization** – Clients must have the necessary KMIP-level permissions to perform specific key management operations. +### Network Requirements -By combining **mTLS for secure communication** and **machine identity-based proxying**, Infisical KMS ensures **strong authentication, controlled access, and centralized key management** for KMIP operations. +Ensure the following network connectivity is in place: -## Setup Instructions +- **KMIP Client → KMIP Server**: KMIP clients must be able to reach the KMIP server on port 5696 (or your configured port). Ensure firewalls allow this traffic and DNS resolution works if using hostnames. -### Setup KMIP for your organization +- **KMIP Server → Infisical Platform**: The KMIP server needs outbound HTTP access to Infisical. For self-hosted instances, ensure connectivity to your custom domain. + +## Configure and Deploy the KMIP Server + +Follow these steps in order to set up KMIP integration with Infisical: - - From there, press Setup KMIP. + + First, you need to enable KMIP for your entire Infisical organization and set up its PKI infrastructure. + + Navigate to **Organization Settings > KMIP** and click **Setup KMIP**. + ![KMIP org navigate](/images/platform/kms/kmip/kmip-org-setup-navigation.png) - - - In the modal, select the desired key algorithm to use for the KMIP PKI of your organization. Press continue. + + In the modal, select the desired key algorithm to use for the KMIP PKI of your organization, then click **Continue**. + ![KMIP org PKI setup](/images/platform/kms/kmip/kmip-org-setup-modal.png) - This generates the KMIP PKI for your organization. After this, you can proceed to setting up your KMIP server. - + This generates the KMIP PKI for your organization, creating the cryptographic foundation that will be used for secure KMIP communications. + + You do not need to manage these certificates yourself; Infisical handles the PKI infrastructure for you. + - -### Deploying and Configuring the KMIP Server + + The KMIP server needs a machine identity to authenticate with Infisical and proxy requests on behalf of clients. -Follow these steps to configure and deploy a KMIP server. + Configure a [machine identity](/documentation/platform/identities/machine-identities#machine-identities) by heading to your organization's **Access Control** and switching over to the **identities** tab. + From there you can click **Create Identity**. - - - Configure a [machine identity](https://infisical.com/docs/documentation/platform/identities/machine-identities#machine-identities) for the KMIP server to use. + This guide assumes you'll be using the [Universal Auth](/documentation/platform/identities/universal-auth) method for the machine identity but you can choose any supported authentication method. + ![KMIP create machine identity](/images/platform/kms/kmip/kmip-create-mi.png) + This machine identity will be used by the KMIP server to authenticate and forward client requests to Infisical KMS. + + + + The machine identity needs permission to proxy KMIP requests. + Create a custom organization role and give it the **Proxy KMIP** permission. + ![KMIP create custom role](/images/platform/kms/kmip/kmip-create-custom-role.png) ![KMIP assign proxy to role](/images/platform/kms/kmip/kmip-assign-custom-role-proxy.png) - Assign the machine identity to the custom organization role. This allows the machine identity to serve KMIP client requests and forward them from your KMIP server to Infisical. - ![KMIP assign role to machine identity](/images/platform/kms/kmip/kmip-assign-mi-to-role.png) - + This permission allows the KMIP server to act as an intermediary between KMIP clients and Infisical. - - To deploy the KMIP server, use the Infisical CLI’s `kmip start` command. - Before proceeding, make sure you have the [Infisical CLI installed](https://infisical.com/docs/cli/overview). + + Now connect the machine identity to the role you just created. - Once installed, launch the KMIP server with the following command: + Assign the machine identity to the custom organization role. + + ![KMIP assign role to machine identity](/images/platform/kms/kmip/kmip-assign-mi-to-role.png) + + This grants the machine identity the ability to serve KMIP client requests and forward them from your KMIP server to Infisical. + + + + Now you're ready to deploy the KMIP server. + You can run the KMIP server on any infrastructure that can reach the Infisical platform, such as a VM or container. + + Once you have your infrastructure ready, you'll need to install the Infisical CLI on the server where you want to run the KMIP server. + + To install the latest Infisical CLI visit [Infisical CLI instructions](https://infisical.com/docs/cli/overview). + If you need to install specific versions of the CLI, you can find them on the [Infisical CLI GitHub Releases](https://github.com/Infisical/cli/releases). + + Then, launch the KMIP server with the following command: ```bash infisical kmip start \ - --identity-client-id= \ # This can be set by defining the INFISICAL_UNIVERSAL_AUTH_CLIENT_ID ENV variable - --identity-client-secret= \ # This can be set by defining the INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET ENV variable - --domain=https://app.infisical.com \ + --identity-client-id=example-client-id \ + --identity-client-secret=example-client-secret \ + --domain=https://my-infisical-instance.com \ + --listen-address="0.0.0.0:5696" \ --hostnames-or-ips="my-kmip-server.com" ``` - The following flags are available for the `infisical kmip start` command:: - - **listen-address** (default: localhost:5696): The address the KMIP server listens on. - - **identity-auth-method** (default: universal-auth): The authentication method for the machine identity. - - **identity-client-id**: The client ID of the machine identity. This can be set by defining the `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` ENV variable. - - **identity-client-secret**: The client secret of the machine identity. This can be set by defining the `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` ENV variable. - - **server-name** (default: "kmip-server"): The name of the KMIP server. - - **certificate-ttl** (default: "1y"): The duration for which the server certificate is valid. - - **hostnames-or-ips:** A comma-separated list of hostnames or IPs the KMIP server will use (required). + **Available flags:** + - **listen-address** (default: localhost:5696): The address the KMIP server listens on. In most cases you'll want to listen on all interfaces (0.0.0.0:5696) + - **identity-auth-method** (default: universal-auth): The authentication method for the machine identity + - **identity-client-id**: The client ID of the machine identity (can be set via `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID` env var) + - **identity-client-secret**: The client secret of the machine identity (can be set via `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET` env var) + - **server-name** (default: "kmip-server"): The name of the KMIP server + - **certificate-ttl** (default: "1y"): The duration for which the server certificate is valid + - **hostnames-or-ips**: The IP address or the hostname of the server where you have deployed the KMIP server. + Once started, your KMIP server is now running and ready to accept client connections. It will authenticate to Infisical using the machine identity and proxy all KMIP operations. - -### Add and Configure KMIP Clients + + Now that the KMIP server is running, you need to register KMIP clients that will connect to it. - - - From there, press Add KMIP Client + Navigate to the desired KMS project if you already have one or create a new project of type KMS, then select **KMIP** once inside the project, and click **Add KMIP Client**. + ![KMIP client overview](/images/platform/kms/kmip/kmip-client-overview.png) - - In the modal, provide the details of your client. The selected permissions determine what KMIP operations can be performed in your KMS project. + + + Define the client and its permissions. + + In the modal, provide the details of your client. The selected permissions determine what KMIP operations (Create, Get, Revoke, etc.) can be performed in your KMS project. + ![KMIP client modal](/images/platform/kms/kmip/kmip-client-modal.png) + + This creates a KMIP client entity in Infisical that will be authenticated via mTLS certificates. - - Once the KMIP client is created, you will have to generate a client certificate. - Press Generate Certificate. + + + Each KMIP client needs its own certificate for mTLS authentication. + + Click **Generate Certificate** for your newly created client. + ![KMIP generate client cert](/images/platform/kms/kmip/kmip-client-generate-cert.png) - Provide the desired TTL and key algorithm to use and press Generate Client Certificate. + Provide the desired TTL (time-to-live) and key algorithm, then click **Generate Client Certificate**. + ![KMIP client cert config](/images/platform/kms/kmip/kmip-client-cert-config-modal.png) - Configure your KMIP clients to use the generated client certificate, certificate chain and private key. + Download the generated client certificate, certificate chain, and private key. + ![KMIP client cert modal](/images/platform/kms/kmip/kmip-client-certificate-modal.png) + Configure your KMIP-compatible applications or tools to use these credentials when connecting to the KMIP server. The client will now authenticate via mTLS and perform authorized key management operations through the KMIP server, which proxies requests to Infisical KMS. -## Additional Resources +## Connecting your KMIP Client to Infisical -- [KMIP 1.4 Specification](http://docs.oasis-open.org/kmip/spec/v1.4/os/kmip-spec-v1.4-os.html) +After completing the setup, configure your KMIP compatible application to connect to the KMIP server. + +While exact configuration steps vary by application, you'll generally need to provide: + +1. **KMIP Server Address**: The hostname or IP and port where your KMIP server is listening (e.g., `my-kmip-server.com:5696`) + +2. **Client Certificates**: The certificate credentials generated from your Infisical KMS project: + - **Client Certificate** (`client-cert.pem`) - Identifies your KMIP client + - **Client Private Key** (`client-key.pem`) - Used for mTLS authentication + - **Certificate Chain** (`cert-chain.pem`) - Verifies the KMIP server + +### General Configuration Steps + + + Determine the address where your KMIP server is accessible. This should match one of the hostnames or IPs you specified when starting the KMIP server with the `--hostnames-or-ips` flag. + + **Example endpoints:** + - `my-kmip-server.com:5696` + - `10.0.1.50:5696` + - `kmip.example.com:5696` + + The default port is `5696`, but this can be changed using the `--listen-address` flag when starting the server. + + + + Organize the certificate materials you downloaded when generating the client certificate from the Infisical KMS project. You should have three files: + + - **client-cert.pem** - The client certificate + - **cert-chain.pem** - The certificate chain (includes intermediate and root CA certificates) + - **client-key.pem** - The private key + + Most KMIP clients require these files in PEM format, which is what Infisical provides by default. + + + + The exact configuration steps vary depending on your KMIP client application. Generally, you'll need to specify: + + **Common configuration parameters:** + - **Server hostname/IP**: Your KMIP server address (e.g., `my-kmip-server.com`) + - **Server port**: Default is `5696` + - **Client certificate**: Path to `client-cert.pem` + - **Client private key**: Path to `client-key.pem` + - **CA certificate**: Path to `cert-chain.pem` (used to verify the server) + - **Protocol version**: KMIP 1.0 through 1.4 are supported + + **Example configuration for PyKMIP:** + ```ini + [client] + host=my-kmip-server.com + port=5696 + certfile=/path/to/client-cert.pem + keyfile=/path/to/client-key.pem + ca_certs=/path/to/cert-chain.pem + ``` + + + + + Once configured, test the connection by performing a simple KMIP operation, such as: + - Querying server capabilities + - Creating a test key + - Listing available keys + + If the connection is successful, your KMIP client is now integrated with Infisical KMS and can perform key management operations according to the permissions you assigned. + + **Troubleshooting connection issues:** + - Verify network connectivity between your KMIP client and the KMIP server + - Check that certificate files are readable and in the correct format + - Ensure the KMIP server is running and accessible + - Review KMIP server logs for authentication errors + - Confirm the client certificate has not expired + + If you require further verification of your certificate details and connectivity to the KMIP server from your KMIP client, you can use the following command from your client machine: + + ```bash + openssl s_client -connect kmip-server-ip-here:5696 --cert /path/to/client-cert.pem --key /path/to/client-cert.pem --CAfile /path/to/cert-chain.pem --tls1_2 --showcerts --state --debug + ``` + + This command attempts to establish a TLS connection to the KMIP server using your client certificate and key, displaying detailed information about the handshake process. + If the connection is successful, you'll see the server's certificate chain and a message indicating that the handshake was completed. + + + diff --git a/docs/images/kmip-diagram.png b/docs/images/kmip-diagram.png new file mode 100644 index 000000000..cc56607a2 Binary files /dev/null and b/docs/images/kmip-diagram.png differ diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 551c79184..edebf1670 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -116,6 +116,27 @@ The platform utilizes Postgres to persist all of its data and Redis for caching Configure the SSL certificate for securing a Postgres connection by first encoding it in base64. Use the following command to encode your certificate: `echo "" | base64` + + Many cloud providers provide a CA certificate for their data regions that you can use to secure your connection with SSL. + + + + If you're hosting your database on AWS RDS, you can use their publicly available CA certificate as the database root certificate. + + You can find all the available CA certificates for AWS RDS on the official [AWS RDS documentation](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html). + + As an example, if your RDS cluster is hosted in `us-east-1` _(US East, N. Virginia)_, you can use the following root certificate: https://truststore.pki.rds.amazonaws.com/us-east-1/us-east-1-bundle.pem. + + All the available CA certificates can be found in the AWS RDS documentation linked above. + + Remember to base64 encode the certificate before setting it as the `DB_ROOT_CERT` environment variable. `cat /path/to/certificate.pem | base64`. + + ```bash + DB_ROOT_CERT=LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1 # .... (base64 encoded certificate) + DB_CONNECTION_URI=?sslmode=verify-ca # or verify-full depending on your security policies + ``` + + diff --git a/docs/self-hosting/deployment-options/kubernetes-helm.mdx b/docs/self-hosting/deployment-options/kubernetes-helm.mdx index f8a2a6864..f4ef67e1e 100644 --- a/docs/self-hosting/deployment-options/kubernetes-helm.mdx +++ b/docs/self-hosting/deployment-options/kubernetes-helm.mdx @@ -1,8 +1,10 @@ --- title: "Kubernetes via Helm Chart" description: "Learn how to use Helm chart to install Infisical on your Kubernetes cluster." ---- +--- + **Prerequisites** + - You have extensive understanding of [Kubernetes](https://kubernetes.io/) - Installed [Helm package manager](https://helm.sh/) version v3.11.3 or greater - You have [kubectl](https://kubernetes.io/docs/reference/kubectl/kubectl/) installed and connected to your kubernetes cluster @@ -12,7 +14,7 @@ description: "Learn how to use Helm chart to install Infisical on your Kubernete ```bash helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/' ``` - ``` + ```bash helm repo update ``` @@ -61,6 +63,7 @@ description: "Learn how to use Helm chart to install Infisical on your Kubernete For production environments, we recommend using Cloud-based Platform as a Service (PaaS) solutions for PostgreSQL and Redis to ensure high availability. In on-premise setups, it's recommended to configure Redis and Postgres for high availability, either by using Bitnami charts or a custom configuration. + ```yaml simple-values-example.yaml apiVersion: v1 kind: Secret @@ -74,6 +77,10 @@ description: "Learn how to use Helm chart to install Infisical on your Kubernete DB_CONNECTION_URI: <> SITE_URL: <> ``` + + + If you need to configure the SSL certificate for your production Postgres instance, you can use the `DB_ROOT_CERT` environment variable. [Learn more about configuring the SSL certificate](/self-hosting/configuration/envars#aws-rds). + diff --git a/frontend/.gitignore b/frontend/.gitignore index a547bf36d..f52343a92 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -22,3 +22,6 @@ dist-ssr *.njsproj *.sln *.sw? + +*storybook.log +storybook-static diff --git a/frontend/.storybook/decorators/DocumentDecorator.tsx b/frontend/.storybook/decorators/DocumentDecorator.tsx new file mode 100644 index 000000000..13f4dc8c7 --- /dev/null +++ b/frontend/.storybook/decorators/DocumentDecorator.tsx @@ -0,0 +1,12 @@ +import { useEffect } from "react"; +import type { Decorator } from "@storybook/react-vite"; + +export const DocumentDecorator: Decorator = (Story) => { + useEffect(() => { + const root = document.getElementsByTagName("html")[0]; + + root.setAttribute("class", "overflow-visible"); + }, []); + + return ; +}; diff --git a/frontend/.storybook/decorators/RouterDecorator.tsx b/frontend/.storybook/decorators/RouterDecorator.tsx new file mode 100644 index 000000000..a559c5cd1 --- /dev/null +++ b/frontend/.storybook/decorators/RouterDecorator.tsx @@ -0,0 +1,17 @@ +import { useMemo } from "react"; +import type { Decorator } from "@storybook/react-vite"; +import { createRootRoute, createRouter, RouterProvider } from "@tanstack/react-router"; + +export const RouterDecorator: Decorator = (Story) => { + const router = useMemo(() => { + const routeTree = createRootRoute({ + component: Story + }); + + return createRouter({ + routeTree + }); + }, [Story]); + + return ; +}; diff --git a/frontend/.storybook/decorators/index.ts b/frontend/.storybook/decorators/index.ts new file mode 100644 index 000000000..7bbb10f15 --- /dev/null +++ b/frontend/.storybook/decorators/index.ts @@ -0,0 +1,2 @@ +export * from "./DocumentDecorator"; +export * from "./RouterDecorator"; diff --git a/frontend/.storybook/main.ts b/frontend/.storybook/main.ts new file mode 100644 index 000000000..0a7b7adb5 --- /dev/null +++ b/frontend/.storybook/main.ts @@ -0,0 +1,14 @@ +import type { StorybookConfig } from "@storybook/react-vite"; + +const config: StorybookConfig = { + stories: [ + "../src/components/v3/**/*.mdx", + "../src/components/v3/**/*.stories.@(js|jsx|mjs|ts|tsx)" + ], + addons: ["@storybook/addon-docs", "@storybook/addon-a11y"], + framework: { + name: "@storybook/react-vite", + options: {} + } +}; +export default config; diff --git a/frontend/.storybook/preview.tsx b/frontend/.storybook/preview.tsx new file mode 100644 index 000000000..d2aaca582 --- /dev/null +++ b/frontend/.storybook/preview.tsx @@ -0,0 +1,36 @@ +import type { Preview } from "@storybook/react-vite"; + +import { DocumentDecorator, RouterDecorator } from "./decorators"; + +import "../src/index.css"; + +const preview: Preview = { + decorators: [DocumentDecorator, RouterDecorator], + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i + } + }, + docs: { + backgroundColor: "var(--background)" + }, + a11y: { + test: "todo" + }, + backgrounds: { + default: "dark", + options: { + dark: { name: "Dark", value: "var(--background)" } + } + } + }, + initialGlobals: { + backgrounds: { + value: "dark" + } + } +}; + +export default preview; diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 00c024056..843a9602e 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -1,3 +1,6 @@ +// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format +import storybook from "eslint-plugin-storybook"; + import js from "@eslint/js"; import globals from "globals"; import reactHooks from "eslint-plugin-react-hooks"; @@ -133,5 +136,6 @@ export default tseslint.config( rules: Object.fromEntries( Object.keys(stylisticPlugin.configs["all-flat"].rules ?? {}).map((key) => [key, "off"]) ) - } + }, + storybook.configs["flat/recommended"] ); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a9f4b070c..6e20b6d35 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -40,7 +40,9 @@ "@radix-ui/react-popper": "^1.2.1", "@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-radio-group": "^1.2.2", + "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.1.3", + "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-toast": "^1.2.3", @@ -57,6 +59,7 @@ "argon2-browser": "^1.18.0", "axios": "^1.12.0", "classnames": "^2.5.1", + "clsx": "^2.1.1", "cva": "npm:class-variance-authority@^0.7.1", "date-fns": "^4.1.0", "dompurify": "^3.2.4", @@ -69,6 +72,7 @@ "jsrp": "^0.2.4", "jwt-decode": "^4.0.0", "lexical": "^0.29.0", + "lucide-react": "^0.544.0", "ms": "^2.1.3", "nprogress": "^0.2.0", "picomatch": "^4.0.2", @@ -98,6 +102,9 @@ "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.15.0", "@kesills/eslint-config-airbnb-typescript": "^20.0.0", + "@storybook/addon-a11y": "^9.1.9", + "@storybook/addon-docs": "^9.1.9", + "@storybook/react-vite": "^9.1.9", "@stylistic/eslint-plugin": "^2.12.1", "@tailwindcss/postcss": "^4.1.14", "@tailwindcss/typography": "^0.5.15", @@ -123,6 +130,7 @@ "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.14", "eslint-plugin-simple-import-sort": "^12.1.1", + "eslint-plugin-storybook": "^9.1.9", "globals": "^15.12.0", "postcss": "^8.4.49", "prettier": "3.4.2", @@ -137,6 +145,14 @@ "vite-tsconfig-paths": "^5.1.4" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -150,38 +166,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz", - "integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", "dev": true, "license": "MIT", "engines": { @@ -189,22 +191,22 @@ } }, "node_modules/@babel/core": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", - "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.0", - "@babel/generator": "^7.26.0", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.0", - "@babel/parser": "^7.26.0", - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.26.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -243,15 +245,15 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -259,14 +261,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", - "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -275,16 +277,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -295,29 +287,38 @@ "semver": "bin/semver.js" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -337,27 +338,27 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, "license": "MIT", "engines": { @@ -365,26 +366,26 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", - "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.10" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", - "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.26.10" + "@babel/types": "^7.28.4" }, "bin": { "parser": "bin/babel-parser.js" @@ -438,54 +439,45 @@ } }, "node_modules/@babel/template": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", - "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", - "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1454,6 +1446,53 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -1467,18 +1506,82 @@ "node": ">=18.0.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "node_modules/@joshwooding/vite-plugin-react-docgen-typescript": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.6.1.tgz", + "integrity": "sha512-J4BaTocTOYFkMHIra1JDWrMWpNmBl4EkplIwHEsV8aeUOtdWjwSnln9U7twjMFTAEB7mptNtSKyVi1Y2W9sDJw==", + "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" + "glob": "^10.0.0", + "magic-string": "^0.30.0", + "react-docgen-typescript": "^2.2.2" + }, + "peerDependencies": { + "typescript": ">= 4.3.x", + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@joshwooding/vite-plugin-react-docgen-typescript/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@joshwooding/vite-plugin-react-docgen-typescript/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@joshwooding/vite-plugin-react-docgen-typescript/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=6.0.0" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, "node_modules/@jridgewell/remapping": { @@ -1501,15 +1604,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -1517,9 +1611,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1822,6 +1916,24 @@ "integrity": "sha512-01d+UjJ8NG7ZStYQxtb8FPzknzGmauG7gEkcH+wHfSdiSQJY9PoBNVSTB9V6F5hAnmFqOxaocTtd7TIEEnzMnA==", "license": "MIT" }, + "node_modules/@mdx-js/react": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", + "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdx": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2170,6 +2282,17 @@ "tsyringe": "^4.8.0" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgr/core": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", @@ -2254,6 +2377,24 @@ } } }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz", @@ -2363,6 +2504,24 @@ } } }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", @@ -2429,6 +2588,24 @@ } } }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", @@ -2652,6 +2829,24 @@ } } }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popover": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.3.tgz", @@ -2689,6 +2884,24 @@ } } }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz", @@ -2792,6 +3005,24 @@ } } }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-progress": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.1.tgz", @@ -2879,6 +3110,171 @@ } } }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-select": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.3.tgz", @@ -2922,7 +3318,7 @@ } } }, - "node_modules/@radix-ui/react-slot": { + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", @@ -2940,6 +3336,39 @@ } } }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-switch": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.2.tgz", @@ -3067,6 +3496,24 @@ } } }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", @@ -3605,6 +4052,297 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@storybook/addon-a11y": { + "version": "9.1.13", + "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-9.1.13.tgz", + "integrity": "sha512-4enIl1h2XSZnFKUQJJoZbp1X40lzdj7f5JE15ZhU1al4z6hHWp7i2zD7ySyDpEbMypBCz1xnLvyiyw79m1fp7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "axe-core": "^4.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.1.13" + } + }, + "node_modules/@storybook/addon-docs": { + "version": "9.1.13", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-9.1.13.tgz", + "integrity": "sha512-V1nCo7bfC3kQ5VNVq0VDcHsIhQf507m+BxMA5SIYiwdJHljH2BXpW2fL3FFn9gv9Wp57AEEzhm+wh4zANaJgkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mdx-js/react": "^3.0.0", + "@storybook/csf-plugin": "9.1.13", + "@storybook/icons": "^1.4.0", + "@storybook/react-dom-shim": "9.1.13", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.1.13" + } + }, + "node_modules/@storybook/builder-vite": { + "version": "9.1.13", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-9.1.13.tgz", + "integrity": "sha512-pmtIjU02ASJOZKdL8DoxWXJgZnpTDgD5WmMnjKJh9FaWmc2YiCW2Y6VRxPox96OM655jYHQe5+UIbk3Cwtwb4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/csf-plugin": "9.1.13", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.1.13", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@storybook/csf-plugin": { + "version": "9.1.13", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-9.1.13.tgz", + "integrity": "sha512-EMpzYuyt9FDcxxfBChWzfId50y8QMpdenviEQ8m+pa6c+ANx3pC5J6t7y0khD8TQu815sTy+nc6cc8PC45dPUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "unplugin": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.1.13" + } + }, + "node_modules/@storybook/global": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz", + "integrity": "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@storybook/icons": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-1.6.0.tgz", + "integrity": "sha512-hcFZIjW8yQz8O8//2WTIXylm5Xsgc+lW9ISLgUk1xGmptIJQRdlhVIXCpSyLrQaaRiyhQRaVg7l3BD9S216BHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta" + } + }, + "node_modules/@storybook/react": { + "version": "9.1.13", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-9.1.13.tgz", + "integrity": "sha512-B0UpYikKf29t8QGcdmumWojSQQ0phSDy/Ne2HYdrpNIxnUvHHUVOlGpq4lFcIDt52Ip5YG5GuAwJg3+eR4LCRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@storybook/react-dom-shim": "9.1.13" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^9.1.13", + "typescript": ">= 4.9.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@storybook/react-dom-shim": { + "version": "9.1.13", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-9.1.13.tgz", + "integrity": "sha512-/tMr9TmV3+98GEQO0S03k4gtKHGCpv9+k9Dmnv+TJK3TBz7QsaFEzMwe3gCgoTaebLACyVveDiZkWnCYAWB6NA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^9.1.13" + } + }, + "node_modules/@storybook/react-vite": { + "version": "9.1.13", + "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-9.1.13.tgz", + "integrity": "sha512-mV1bZ1bpkNQygnuDo1xMGAS5ZXuoXFF0WGmr/BzNDGmRhZ1K1HQh42kC0w3PklckFBUwCFxmP58ZwTFzf+/dJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@joshwooding/vite-plugin-react-docgen-typescript": "0.6.1", + "@rollup/pluginutils": "^5.0.2", + "@storybook/builder-vite": "9.1.13", + "@storybook/react": "9.1.13", + "find-up": "^7.0.0", + "magic-string": "^0.30.0", + "react-docgen": "^8.0.0", + "resolve": "^1.22.8", + "tsconfig-paths": "^4.2.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^9.1.13", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@storybook/react-vite/node_modules/find-up": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", + "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.2.0", + "path-exists": "^5.0.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@storybook/react-vite/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@storybook/react-vite/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@storybook/react-vite/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@storybook/react-vite/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@storybook/react-vite/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/@storybook/react-vite/node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@storybook/react-vite/node_modules/yocto-queue": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@stylistic/eslint-plugin": { "version": "2.13.0", "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.13.0.tgz", @@ -4441,6 +5179,82 @@ "zod": "^3.23.8" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@types/argon2-browser": { "version": "1.18.4", "resolved": "https://registry.npmjs.org/@types/argon2-browser/-/argon2-browser-1.18.4.tgz", @@ -4448,6 +5262,14 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4484,13 +5306,25 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.6", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", - "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.20.7" + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, "node_modules/@types/d3-color": { @@ -4556,6 +5390,21 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/doctrine": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", + "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -4607,6 +5456,13 @@ "@types/unist": "*" } }, + "node_modules/@types/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ms": { "version": "0.7.34", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", @@ -4710,6 +5566,13 @@ "@types/react": "*" } }, + "node_modules/@types/resolve": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", + "integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -5032,6 +5895,107 @@ "vite": "^4 || ^5 || ^6" } }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@xyflow/react": { "version": "12.4.4", "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.4.4.tgz", @@ -5424,6 +6388,31 @@ "util": "^0.12.5" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -5459,7 +6448,6 @@ "integrity": "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==", "dev": true, "license": "MPL-2.0", - "peer": true, "engines": { "node": ">=4" } @@ -5561,12 +6549,36 @@ ], "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.19.tgz", + "integrity": "sha512-zoKGUdu6vb2jd3YOq0nnhEDQVbPcHhco3UImJrv5dSkvxTc2pl2WjOPsjZXDwPDSl5eghIMuY3R6J9NDKF3KcQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/before-after-hook": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", "license": "Apache-2.0" }, + "node_modules/better-opn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz", + "integrity": "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "open": "^8.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -5777,9 +6789,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", - "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", "dev": true, "funding": [ { @@ -5797,10 +6809,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001669", - "electron-to-chromium": "^1.5.41", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.1" + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -5914,9 +6927,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001688", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001688.tgz", - "integrity": "sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA==", + "version": "1.0.30001751", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", + "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", "dev": true, "funding": [ { @@ -5970,6 +6983,24 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -6023,6 +7054,17 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -6399,6 +7441,14 @@ "utrie": "^1.0.2" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -6646,6 +7696,17 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -6670,6 +7731,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -6782,6 +7854,14 @@ "node": ">=6.0.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -6827,10 +7907,17 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { - "version": "1.5.73", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.73.tgz", - "integrity": "sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg==", + "version": "1.5.237", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz", + "integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==", "dev": true, "license": "ISC" }, @@ -6862,8 +7949,7 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/enhanced-resolve": { "version": "5.18.3", @@ -7100,6 +8186,20 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -7570,6 +8670,23 @@ "eslint": ">=5.0.0" } }, + "node_modules/eslint-plugin-storybook": { + "version": "9.1.13", + "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-9.1.13.tgz", + "integrity": "sha512-kPuhbtGDiJLB5OLZuwFZAxgzWakNDw64sJtXUPN8g0+VAeXfHyZEmsE28qIIETHxtal71lPKVm8QNnERaJHPJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.8.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "eslint": ">=8", + "storybook": "^9.1.13" + } + }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -7694,6 +8811,21 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -7995,6 +9127,23 @@ "is-callable": "^1.1.3" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", @@ -8768,6 +9917,17 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -9034,6 +10194,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -9339,6 +10516,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -9392,6 +10583,22 @@ "node": ">= 0.4" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -9910,6 +11117,44 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.544.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.544.0.tgz", + "integrity": "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.19", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", @@ -10591,6 +11836,17 @@ "node": ">= 0.6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -10716,9 +11972,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.26", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", + "integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==", "dev": true, "license": "MIT" }, @@ -10929,6 +12185,25 @@ "wrappy": "1" } }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -10995,6 +12270,13 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -11140,6 +12422,30 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -11149,6 +12455,17 @@ "node": ">=8" } }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 14.16" + } + }, "node_modules/pbkdf2": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", @@ -11406,6 +12723,44 @@ } } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/prismjs": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", @@ -11686,6 +13041,38 @@ "react": ">=16.8.0" } }, + "node_modules/react-docgen": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-8.0.2.tgz", + "integrity": "sha512-+NRMYs2DyTP4/tqWz371Oo50JqmWltR1h2gcdgUMAWZJIAvrd0/SqlCfx7tpzpl/s36rzw6qH2MjoNrxtRNYhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.2", + "@types/babel__core": "^7.20.5", + "@types/babel__traverse": "^7.20.7", + "@types/doctrine": "^0.0.9", + "@types/resolve": "^1.20.2", + "doctrine": "^3.0.0", + "resolve": "^1.22.1", + "strip-indent": "^4.0.0" + }, + "engines": { + "node": "^20.9.0 || >=22" + } + }, + "node_modules/react-docgen-typescript": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.4.0.tgz", + "integrity": "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">= 4.3.x" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -11985,12 +13372,70 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/recast": { + "version": "0.23.11", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/recast/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/redaxios": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/redaxios/-/redaxios-0.5.1.tgz", "integrity": "sha512-FSD2AmfdbkYwl7KDExYQlVvIrFz6Yd83pGfaGjBzM9F6rpq8g652Q4Yq5QD4c+nf4g2AgeElv1y+8ajUPiOYMg==", "license": "Apache-2.0" }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redent/node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -12500,6 +13945,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -12545,6 +14003,43 @@ "node": ">=0.1.14" } }, + "node_modules/storybook": { + "version": "9.1.13", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.13.tgz", + "integrity": "sha512-G3KZ36EVzXyHds72B/qtWiJnhUpM0xOUeYlDcO9DSHL1bDTv15cW4+upBl+mcBZrDvU838cn7Bv4GpF+O5MCfw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@storybook/global": "^5.0.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/user-event": "^14.6.1", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/spy": "3.2.4", + "better-opn": "^3.0.2", + "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0", + "esbuild-register": "^3.5.0", + "recast": "^0.23.5", + "semver": "^7.6.2", + "ws": "^8.18.0" + }, + "bin": { + "storybook": "bin/index.cjs" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "prettier": "^2 || ^3" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + } + } + }, "node_modules/stream-browserify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", @@ -12579,6 +14074,76 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -12719,6 +14284,20 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -12729,6 +14308,19 @@ "node": ">=4" } }, + "node_modules/strip-indent": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz", + "integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -12904,6 +14496,28 @@ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", "license": "MIT" }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-buffer": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", @@ -12968,6 +14582,16 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/tsconfck": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.4.tgz", @@ -13660,6 +15284,19 @@ "dev": true, "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -13762,9 +15399,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { @@ -13783,7 +15420,7 @@ "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -14266,6 +15903,107 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -14273,6 +16011,29 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index d89025650..9c375190e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,7 +9,9 @@ "preview": "vite preview", "lint": "eslint ./src", "lint:fix": "eslint --fix ./src", - "type:check": "tsc --noEmit --project ./tsconfig.app.json" + "type:check": "tsc --noEmit --project ./tsconfig.app.json", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" }, "overrides": { "sha.js": "2.4.12" @@ -47,7 +49,9 @@ "@radix-ui/react-popper": "^1.2.1", "@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-radio-group": "^1.2.2", + "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.1.3", + "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-toast": "^1.2.3", @@ -64,6 +68,7 @@ "argon2-browser": "^1.18.0", "axios": "^1.12.0", "classnames": "^2.5.1", + "clsx": "^2.1.1", "cva": "npm:class-variance-authority@^0.7.1", "date-fns": "^4.1.0", "dompurify": "^3.2.4", @@ -76,6 +81,7 @@ "jsrp": "^0.2.4", "jwt-decode": "^4.0.0", "lexical": "^0.29.0", + "lucide-react": "^0.544.0", "ms": "^2.1.3", "nprogress": "^0.2.0", "picomatch": "^4.0.2", @@ -105,6 +111,9 @@ "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.15.0", "@kesills/eslint-config-airbnb-typescript": "^20.0.0", + "@storybook/addon-a11y": "^9.1.9", + "@storybook/addon-docs": "^9.1.9", + "@storybook/react-vite": "^9.1.9", "@stylistic/eslint-plugin": "^2.12.1", "@tailwindcss/postcss": "^4.1.14", "@tailwindcss/typography": "^0.5.15", @@ -130,6 +139,7 @@ "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.14", "eslint-plugin-simple-import-sort": "^12.1.1", + "eslint-plugin-storybook": "^9.1.9", "globals": "^15.12.0", "postcss": "^8.4.49", "prettier": "3.4.2", diff --git a/frontend/public/fonts/AllianceNo2-Regular.otf b/frontend/public/fonts/AllianceNo2-Regular.otf new file mode 100644 index 000000000..a51796093 Binary files /dev/null and b/frontend/public/fonts/AllianceNo2-Regular.otf differ diff --git a/frontend/public/images/logotransparent_trimmed.png b/frontend/public/images/logotransparent_trimmed.png new file mode 100644 index 000000000..32911fb83 Binary files /dev/null and b/frontend/public/images/logotransparent_trimmed.png differ diff --git a/frontend/public/lotties/infisical_loading_white.json b/frontend/public/lotties/infisical_loading_white.json new file mode 100644 index 000000000..ee2b15c3b --- /dev/null +++ b/frontend/public/lotties/infisical_loading_white.json @@ -0,0 +1 @@ +{"v":"5.7.5","fr":100,"ip":0,"op":300,"w":800,"h":420,"nm":"Comp 1","ddd":0,"metadata":{},"assets":[{"id":"0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Vector (Stroke)","sr":1,"ks":{"p":{"a":0,"k":[-205,-431],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","nm":"Vector (Stroke)","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[20.447,-130.148],[-77.721,-88.116],[-139.588,-16.199],[-148.936,-1.772],[-139.006,12.261],[-71.202,92.517],[20.447,130.148],[148.936,0],[20.447,-130.148]],"i":[[0,0],[29.018,-24.696],[16.327,-25.197],[0,0],[0,0],[-23.672,-20.706],[-35.524,0],[0,71.767],[71.074,0]],"o":[[-35.542,0],[-24.403,20.768],[0,0],[0,0],[22.425,31.688],[26.864,23.498],[71.074,0],[0,-71.768],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-61.273,-68.789],[-77.721,-88.116],[-61.273,-68.789]],"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 3","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-44.825,-49.463],[20.447,-79.392],[98.18,0],[20.447,79.392],[-37.785,54.313],[-87.278,-2.751],[-44.825,-49.463],[-44.825,-49.463]],"i":[[0,0],[-18.899,0],[0,-44.371],[42.411,0],[20.197,17.666],[17.78,24.244],[-15.565,13.246],[0,0]],"o":[[24.072,-20.487],[42.411,0],[0,44.371],[-20.186,0],[-15.87,-13.881],[12.425,-17.11],[0,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gf","o":{"a":0,"k":100,"ix":2},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,1,1,0.5,1,1,1,1,1,1,1,0,1,0.5,0.5,1,0],"ix":2}},"s":{"a":0,"k":[-86.28473663330078,1.4994840621948242],"ix":2},"e":{"a":0,"k":[462.44384765625,-5.23858642578125],"ix":2},"t":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[747.4509887695312,615.6649780273438],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[140.5150055885315,140.5150055885315],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Vector (Stroke)","sr":1,"ks":{"p":{"a":0,"k":[-205,-431],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","nm":"Vector (Stroke)","it":[{"ty":"gr","nm":"Path 1","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-98.468,0],[-20.444,-79.392],[46.888,-43.887],[46.888,-43.887],[87.062,-0.165],[48.675,41.472],[65.75,60.248],[48.675,41.472],[-20.444,79.392],[-98.468,0]],"i":[[0,0],[-42.675,0],[-27.224,-24.453],[0,0],[-11.056,-14.336],[14.986,-13.628],[0,0],[0,0],[15.764,0],[0,44.267]],"o":[[0,-44.266],[16.468,0],[0,0],[15.303,13.745],[-10.159,13.06],[0,0],[0,0],[-28.618,26.025],[-42.675,0],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gr","nm":"Path 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[-20.444,-130.149],[-149.224,0],[-20.444,130.149],[82.824,79.024],[82.824,79.023],[139.462,14.105],[149.224,-0.255],[139.377,-14.556],[80.805,-81.647],[-20.444,-130.149]],"i":[[0,0],[0,-71.764],[-71.238,0],[-30.616,27.842],[0,0],[-12.465,18.334],[0,0],[0,0],[24.224,21.758],[35.661,0]],"o":[[-71.238,0],[0,71.764],[35.674,0],[0,0],[24.139,-21.952],[0,0],[0,0],[-13.86,-20.129],[-29.959,-26.91],[0,0]]}}},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"gf","o":{"a":0,"k":100,"ix":2},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,1,1,0.5,1,1,1,1,1,1,1,0,1,0.5,0.5,1,0],"ix":2}},"s":{"a":0,"k":[62.10511016845703,5.982990741729736],"ix":2},"e":{"a":0,"k":[-461.3863220214844,-3.3000035285949707],"ix":2},"t":1},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]},{"ty":"tr","p":{"a":0,"k":[414.8349914550781,614.260009765625],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[140.5150055885315,140.5150055885315],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":301,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"shapes":[{"ty":"gr","nm":"Shape Layer 2","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"c":true,"v":[[64.219,60.955],[75.017,50.459],[64.706,38.974],[56.737,41.937],[52.529,45.908],[46.994,53.295],[43.203,57.056],[35.02,60.825],[24.169,49.937],[34.908,38.936],[42.707,42.246],[47.079,46.558],[64.219,60.955]],"i":[[0,0],[-0.155,3.832],[5.611,0.221],[2.488,-1.995],[1.254,-1.476],[2.012,-2.367],[1.531,-1.317],[2.721,0.003],[0,5.642],[-6.195,0.163],[-2.204,-1.755],[-1.435,-1.717],[-7.72,0.139]],"o":[[7.629,-0.137],[0.248,-6.111],[-2.688,-0.106],[-1.511,1.212],[-2.012,2.368],[-1.153,1.356],[-3.063,2.455],[-6.495,-0.007],[0,-5.642],[2.954,-0.078],[1.264,1.149],[4.489,5.371],[0,0]]}}},{"ty":"tm","s":{"a":0,"k":0,"ix":2},"e":{"a":0,"k":20,"ix":2},"o":{"a":1,"k":[{"t":0,"s":[61.00000000000001],"i":{"x":[0.833],"y":[0.904]},"o":{"x":[0.167],"y":[0.167]}},{"t":50,"s":[149],"i":{"x":[0.833],"y":[0.715]},"o":{"x":[0.167],"y":[0.258]}},{"t":153,"s":[216.99999999999997],"i":{"x":[0.833],"y":[0.889]},"o":{"x":[0.167],"y":[0.092]}},{"t":207,"s":[325.00000000000006],"i":{"x":[0.833],"y":[0.81]},"o":{"x":[0.167],"y":[0.25]}},{"t":287,"s":[397.00000000000006],"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.095]}},{"t":300,"s":[421.00000000000006],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"m":1},{"ty":"gs","o":{"a":0,"k":100,"ix":2},"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,1,1,0.5,1,1,1,1,1,1,1,0,1,0.5,0.5,1,0],"ix":2}},"s":{"a":0,"k":[49.759,50.221],"ix":2},"e":{"a":1,"k":[{"t":0,"s":[120.553,-22.385],"i":{"x":[0.692],"y":[0.616]},"o":{"x":[0.362],"y":[0]}},{"t":67,"s":[65.727,-39.14],"i":{"x":[0.682],"y":[1]},"o":{"x":[0.359],"y":[0.551]}},{"t":150,"s":[8.856,-27.571],"i":{"x":[0.653],"y":[0.47]},"o":{"x":[0.329],"y":[0]}},{"t":227,"s":[65.202,-32.204],"i":{"x":[0.637],"y":[1]},"o":{"x":[0.31],"y":[0.455]}},{"t":300,"s":[120.553,-22.385],"i":{"x":[0.75],"y":[0.75]},"o":{"x":[0.25],"y":[0.25]}}],"ix":2},"t":1,"w":{"a":0,"k":5.4,"ix":2},"lc":1,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[400.3317565917969,210.40078735351562],"ix":2},"a":{"a":0,"k":[49.59747009478117,49.94493849689434],"ix":2},"s":{"a":0,"k":[1339.9999618530273,1339.9999618530273],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}}]}],"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":3,"nm":"","sr":1,"ks":{"p":{"a":0,"k":[400.0003356933594,210.00125122070312],"ix":2},"a":{"a":0,"k":[580.941,614.963],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":100,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"refId":"0","w":752,"h":368,"ind":5,"ty":0,"nm":"Vector (Stroke) :M","sr":1,"ks":{"p":{"a":0,"k":[205,431],"ix":2},"a":{"a":0,"k":[0,0],"ix":2},"s":{"a":0,"k":[100,100],"ix":2},"r":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":33,"ix":2},"sk":{"a":0,"k":0,"ix":2},"sa":{"a":0,"k":0,"ix":2}},"ao":0,"ip":0,"op":301,"st":0,"bm":0,"parent":4}],"markers":[]} \ No newline at end of file diff --git a/frontend/src/components/license/UpgradePlanModal/UpgradePlanModal.tsx b/frontend/src/components/license/UpgradePlanModal/UpgradePlanModal.tsx index 17c0777f1..2279d6f8d 100644 --- a/frontend/src/components/license/UpgradePlanModal/UpgradePlanModal.tsx +++ b/frontend/src/components/license/UpgradePlanModal/UpgradePlanModal.tsx @@ -8,22 +8,40 @@ type Props = { isOpen?: boolean; onOpenChange?: (isOpen: boolean) => void; text: string; + isEnterpriseFeature?: boolean; }; -export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Element => { +export const UpgradePlanModal = ({ + text, + isOpen, + onOpenChange, + isEnterpriseFeature = false +}: Props): JSX.Element => { const { subscription } = useSubscription(); const { currentOrg } = useOrganization(); const { mutateAsync, isPending } = useGetOrgTrialUrl(); - const link = - subscription && subscription.slug !== null - ? ("/organization/billing" as const) - : "https://infisical.com/scheduledemo"; + + const getLink = () => { + // self-hosting + if (!subscription || subscription.slug === null) { + return "https://infisical.com/scheduledemo"; + } + + // Infisical cloud + if (isEnterpriseFeature) { + return "https://infisical.com/talk-to-us"; + } + + return "/organization/billing" as const; + }; + + const link = getLink(); const handleUpgradeBtnClick = async () => { try { if (!subscription || !currentOrg) return; - if (!subscription.has_used_trial) { + if (!subscription.has_used_trial && !isEnterpriseFeature) { // direct user to start pro trial const url = await mutateAsync({ @@ -40,6 +58,17 @@ export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Ele console.error(err); } }; + const getUpgradePlanLabel = () => { + if (subscription) { + if (isEnterpriseFeature) { + return "Talk to Us"; + } + if (!subscription.has_used_trial) { + return "Start Pro Free Trial"; + } + } + return "Upgrade Plan"; + }; return ( @@ -55,7 +84,7 @@ export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Ele onClick={handleUpgradeBtnClick} className="mr-4" > - {subscription && !subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"} + {getUpgradePlanLabel()} + ) + }, + parameters: { + docs: { + description: { + story: + "Use the `asChild` prop with a `button` tag to use a badge as a button. Do not use a styled `Button` component." + } + } + } +}; + +export const IsTruncatable: Story = { + name: "Example: isTruncatable", + args: { + isTruncatable: true, + children: ( + <> + + Infisical Infrastructure + + ) + }, + parameters: { + docs: { + description: { + story: + "Use the `isTruncatable` prop with a `span` tag wrapping the text content to support truncation." + } + } + }, + decorators: (Story) => ( +
+ +
+ ) +}; diff --git a/frontend/src/components/v3/generic/Badge/Badge.tsx b/frontend/src/components/v3/generic/Badge/Badge.tsx new file mode 100644 index 000000000..cc66806bc --- /dev/null +++ b/frontend/src/components/v3/generic/Badge/Badge.tsx @@ -0,0 +1,58 @@ +import { forwardRef } from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "cva"; + +import { cn } from "@app/components/v3/utils"; + +const badgeVariants = cva( + [ + "select-none items-center rounded-sm px-1.5 py-0.5 text-xs", + "gap-x-1 [a&,button&]:cursor-pointer inline-flex", + "[&>svg]:pointer-events-none [&>svg]:shrink-0 [&>svg]:stroke-[2.25] [&>svg]:size-3", + "transition duration-200 ease-in-out" + ], + { + variants: { + isTruncatable: { + true: "[&>span,&>p]:truncate min-w-0", + false: "w-fit shrink-0 whitespace-nowrap overflow-hidden" + }, + variant: { + neutral: "bg-neutral/30 text-neutral [a&,button&]:hover:bg-neutral/40", + success: "bg-success/30 text-success [a&,button&]:hover:bg-success/40", + info: "bg-info/30 text-info [a&,button&]:hover:bg-info/40", + warning: "bg-warning/30 text-warning [a&,button&]:hover:bg-warning/40", + danger: "bg-danger/30 text-danger [a&,button&]:hover:bg-danger/40", + project: "bg-project/30 text-project [a&,button&]:hover:bg-project/40", + org: "bg-org/30 text-org [a&,button&]:hover:bg-org/40", + "sub-org": "bg-sub-org/30 text-sub-org [a&,button&]:hover:bg-sub-org/40" + } + }, + defaultVariants: { + variant: "success" + } + } +); + +type TBadgeProps = VariantProps & + React.ComponentProps<"span"> & { + asChild?: boolean; + }; + +const Badge = forwardRef( + ({ className, variant, asChild = false, isTruncatable = false, ...props }, ref): JSX.Element => { + const Comp = asChild ? Slot : "span"; + return ( + + ); + } +); + +Badge.displayName = "Badge"; + +export { Badge, badgeVariants, type TBadgeProps }; diff --git a/frontend/src/components/v3/generic/Badge/index.ts b/frontend/src/components/v3/generic/Badge/index.ts new file mode 100644 index 000000000..ae21190ba --- /dev/null +++ b/frontend/src/components/v3/generic/Badge/index.ts @@ -0,0 +1 @@ +export * from "./Badge"; diff --git a/frontend/src/components/v3/generic/index.ts b/frontend/src/components/v3/generic/index.ts new file mode 100644 index 000000000..ae21190ba --- /dev/null +++ b/frontend/src/components/v3/generic/index.ts @@ -0,0 +1 @@ +export * from "./Badge"; diff --git a/frontend/src/components/v3/index.ts b/frontend/src/components/v3/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/components/v3/utils/index.ts b/frontend/src/components/v3/utils/index.ts new file mode 100644 index 000000000..365058ceb --- /dev/null +++ b/frontend/src/components/v3/utils/index.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/frontend/src/const/routes.ts b/frontend/src/const/routes.ts index 410df7440..c9a6b326d 100644 --- a/frontend/src/const/routes.ts +++ b/frontend/src/const/routes.ts @@ -62,7 +62,11 @@ export const ROUTE_PATHS = Object.freeze({ "/organization/app-connections/$appConnection/oauth/callback", "/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback" ) - } + }, + NetworkingPage: setRoute( + "/organization/networking", + "/_authenticate/_inject-org-details/_org-layout/organization/networking" + ) }, SecretManager: { ApprovalPage: setRoute( diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 6167f6e07..e9e80feec 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -31,6 +31,7 @@ export * from "./pkiSubscriber"; export * from "./projects"; export * from "./projectUserAdditionalPrivilege"; export * from "./rateLimit"; +export * from "./relays"; export * from "./roles"; export * from "./scim"; export * from "./secretApproval"; diff --git a/frontend/src/hooks/api/organization/types.ts b/frontend/src/hooks/api/organization/types.ts index e9c36fada..867450830 100644 --- a/frontend/src/hooks/api/organization/types.ts +++ b/frontend/src/hooks/api/organization/types.ts @@ -66,6 +66,8 @@ export type PlanBillingInfo = { interval: "month" | "year"; intervalCount: number; quantity: number; + users: number; + identities: number; }; export type Invoice = { diff --git a/frontend/src/hooks/api/pam/enums.ts b/frontend/src/hooks/api/pam/enums.ts index b6e5ce64c..0684f6073 100644 --- a/frontend/src/hooks/api/pam/enums.ts +++ b/frontend/src/hooks/api/pam/enums.ts @@ -1,5 +1,6 @@ export enum PamResourceType { Postgres = "postgres", + MySQL = "mysql", RDP = "rdp", SSH = "ssh", Kubernetes = "kubernetes" diff --git a/frontend/src/hooks/api/pam/maps.ts b/frontend/src/hooks/api/pam/maps.ts index a27507d7d..c240a12ad 100644 --- a/frontend/src/hooks/api/pam/maps.ts +++ b/frontend/src/hooks/api/pam/maps.ts @@ -5,6 +5,7 @@ export const PAM_RESOURCE_TYPE_MAP: Record< { name: string; image: string; size?: number } > = { [PamResourceType.Postgres]: { name: "PostgreSQL", image: "Postgres.png" }, + [PamResourceType.MySQL]: { name: "MySQL", image: "MySql.png" }, [PamResourceType.RDP]: { name: "RDP", image: "RDP.png" }, [PamResourceType.SSH]: { name: "SSH", image: "SSH.png" }, [PamResourceType.Kubernetes]: { name: "Kubernetes", image: "Kubernetes.png" } diff --git a/frontend/src/hooks/api/pam/types/index.ts b/frontend/src/hooks/api/pam/types/index.ts index 1b4acf6d4..23cdf389d 100644 --- a/frontend/src/hooks/api/pam/types/index.ts +++ b/frontend/src/hooks/api/pam/types/index.ts @@ -1,11 +1,13 @@ import { PamResourceType, PamSessionStatus } from "../enums"; import { TPostgresAccount, TPostgresResource } from "./postgres-resource"; +import { TMySQLAccount, TMySQLResource } from "./mysql-resource"; export * from "./postgres-resource"; +export * from "./mysql-resource"; -export type TPamResource = TPostgresResource; +export type TPamResource = TPostgresResource | TMySQLResource; -export type TPamAccount = TPostgresAccount; +export type TPamAccount = TPostgresAccount | TMySQLAccount; export type TPamFolder = { id: string; diff --git a/frontend/src/hooks/api/pam/types/mysql-resource.ts b/frontend/src/hooks/api/pam/types/mysql-resource.ts new file mode 100644 index 000000000..f1a7c09c8 --- /dev/null +++ b/frontend/src/hooks/api/pam/types/mysql-resource.ts @@ -0,0 +1,14 @@ +import { PamResourceType } from "../enums"; +import { TBaseSqlConnectionDetails, TBaseSqlCredentials } from "./shared/sql-resource"; +import { TBasePamAccount } from "./base-account"; +import { TBasePamResource } from "./base-resource"; + +// Resources +export type TMySQLResource = TBasePamResource & { resourceType: PamResourceType.MySQL } & { + connectionDetails: TBaseSqlConnectionDetails; +}; + +// Accounts +export type TMySQLAccount = TBasePamAccount & { + credentials: TBaseSqlCredentials; +}; diff --git a/frontend/src/index.css b/frontend/src/index.css index 7c9b7db9b..2a1da3fa2 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -37,12 +37,26 @@ } @theme { - /*legacy color schema */ /* Fonts */ --font-inter: "Inter", sans-serif; + --max-width-8xl: 88rem; /* 1408px */ + + /* Colors v2 */ + --color-background: #19191c; + --color-foreground: white; + --color-success: #2ecc71; + --color-info: #34c2db; + --color-warning: #f1c40f; + --color-danger: #e74c3c; + --color-org: #30B3FF; + --color-sub-org: #96ff59; + --color-project: #e0ed34; + --color-neutral: #adaeb0; + + /*legacy color schema */ --color-org-v1: #30B3FF; --color-namespace-v1: #96ff59; - --max-width-8xl: 88rem; /* 1408px */ + /* Primary */ --color-primary-50: #fffff5; --color-primary-100: #fcfce8; diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx index c92844eed..dc0dffb0d 100644 --- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx +++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateIssuanceModal.tsx @@ -111,6 +111,7 @@ type TCertificateDetails = { export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId }: Props) => { const [certificateDetails, setCertificateDetails] = useState(null); + const [shouldShowSubjectSection, setShouldShowSubjectSection] = useState(true); const { currentProject } = useProject(); const inputSerialNumber = @@ -127,19 +128,9 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId } const { mutateAsync: createCertificate } = useCreateCertificateV3(); - const selectedProfileId = useMemo(() => { - const form = document.querySelector('select[name="profileId"]') as HTMLSelectElement; - return form?.value || profileId || ""; - }, [profileId]); - - const selectedProfile = useMemo( - () => profilesData?.certificateProfiles?.find((p) => p.id === selectedProfileId), - [profilesData?.certificateProfiles, selectedProfileId] - ); - - const { data: templateData } = useGetCertificateTemplateV2ById({ - templateId: selectedProfile?.certificateTemplateId || "" - }); + const formResolver = useMemo(() => { + return zodResolver(createSchema(shouldShowSubjectSection)); + }, [shouldShowSubjectSection]); const { control, @@ -150,7 +141,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId } formState, formState: { isSubmitting } } = useForm({ - resolver: zodResolver(createSchema((templateData?.subject?.length || 0) > 0)), + resolver: formResolver, defaultValues: { profileId: profileId || "", subjectAttributes: [], @@ -169,6 +160,16 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId } [profilesData?.certificateProfiles, actualSelectedProfileId] ); + const { data: templateData } = useGetCertificateTemplateV2ById({ + templateId: actualSelectedProfile?.certificateTemplateId || "" + }); + + useEffect(() => { + if (templateData !== undefined) { + setShouldShowSubjectSection((templateData?.subject?.length || 0) > 0); + } + }, [templateData]); + const { constraints, filteredKeyUsages, @@ -186,6 +187,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle, profileId } const resetAllState = useCallback(() => { setCertificateDetails(null); + setShouldShowSubjectSection(true); resetConstraints(); reset(); }, [reset, resetConstraints]); diff --git a/frontend/src/pages/organization/BillingPage/components/BillingCloudTab/PreviewSection.tsx b/frontend/src/pages/organization/BillingPage/components/BillingCloudTab/PreviewSection.tsx index d22eab08b..7edc8b0db 100644 --- a/frontend/src/pages/organization/BillingPage/components/BillingCloudTab/PreviewSection.tsx +++ b/frontend/src/pages/organization/BillingPage/components/BillingCloudTab/PreviewSection.tsx @@ -1,10 +1,10 @@ import { useEffect } from "react"; -import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; +import { faArrowUpRightFromSquare, faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useQueryClient } from "@tanstack/react-query"; import { OrgPermissionCan } from "@app/components/permissions"; -import { Button } from "@app/components/v2"; +import { Button, Tooltip } from "@app/components/v2"; import { OrgPermissionBillingActions, OrgPermissionSubjects, @@ -27,6 +27,9 @@ export const PreviewSection = () => { const { subscription } = useSubscription(true); const queryClient = useQueryClient(); const { data, isPending } = useGetOrgPlanBillingInfo(currentOrg?.id ?? ""); + + const totalAmount = data?.amount ? data.amount * (data.users + data.identities) : 0; + const getOrgTrialUrl = useGetOrgTrialUrl(); const createCustomerPortalSession = useCreateCustomerPortalSession(); @@ -190,14 +193,28 @@ export const PreviewSection = () => { )} -
-

Price

-

- {subscription.status === "trialing" - ? "$0.00 / month" - : `${formatAmount(data.amount)} / ${data.interval}`} -

-
+ {subscription.slug !== "enterprise" ? ( +
+

Price

+

+ {subscription.status === "trialing" ? ( + "$0.00 / month" + ) : ( + <> + {formatAmount(totalAmount)} / {data.interval} + {(subscription.slug === "pro" || subscription.slug === "pro-annual") && ( + 1 ? "users" : "user"} and ${data.identities} ${data.identities > 1 ? "machine identities" : "machine identity"}.`} + className="max-w-lg" + > + + + )} + + )} +

+
+ ) : null}

Subscription renews on

diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/GatewayTab.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/GatewayTab.tsx index 27ebb6820..a2a24015f 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/GatewayTab.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/GatewayTab.tsx @@ -8,6 +8,7 @@ import { faEllipsisV, faInfoCircle, faMagnifyingGlass, + faPlus, faSearch, faTrash } from "@fortawesome/free-solid-svg-icons"; @@ -17,6 +18,7 @@ import { useQuery } from "@tanstack/react-query"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { + Button, DeleteActionModal, DropdownMenu, DropdownMenuContent, @@ -47,6 +49,7 @@ import { gatewaysQueryKeys, useDeleteGatewayById } from "@app/hooks/api/gateways import { useDeleteGatewayV2ById } from "@app/hooks/api/gateways-v2"; import { EditGatewayDetailsModal } from "./components/EditGatewayDetailsModal"; +import { GatewayDeployModal } from "./components/GatewayDeployModal"; const GatewayHealthStatus = ({ heartbeat }: { heartbeat?: string }) => { const heartbeatDate = heartbeat ? new Date(heartbeat) : null; @@ -73,6 +76,7 @@ export const GatewayTab = withPermission( const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ + "deployGateway", "deleteGateway", "editDetails" ] as const); @@ -101,8 +105,8 @@ export const GatewayTab = withPermission( return (

-
-
+
+

Gateways

+
+

@@ -257,6 +269,10 @@ export const GatewayTab = withPermission( deleteKey="confirm" onDeleteApproved={() => handleDeleteGateway()} /> + handlePopUpToggle("deployGateway", isOpen)} + />

diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx new file mode 100644 index 000000000..7537d95a3 --- /dev/null +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayCliDeploymentMethod.tsx @@ -0,0 +1,387 @@ +import { useMemo, useState } from "react"; +import { SingleValue } from "react-select"; +import { faCopy, faQuestionCircle, faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useNavigate } from "@tanstack/react-router"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + Checkbox, + FilterableSelect, + FormLabel, + IconButton, + Input, + ModalClose, + Tooltip +} from "@app/components/v2"; +import { ROUTE_PATHS } from "@app/const/routes"; +import { + OrgPermissionIdentityActions, + OrgPermissionSubjects, + useOrganization, + useOrgPermission +} from "@app/context"; +import { + useAddIdentityTokenAuth, + useCreateTokenIdentityTokenAuth, + useGetIdentityMembershipOrgs, + useGetIdentityTokenAuth, + useGetRelays +} from "@app/hooks/api"; +import { slugSchema } from "@app/lib/schemas"; + +import { RelayOption } from "./RelayOption"; + +const baseFormSchema = z.object({ + name: slugSchema({ field: "name" }), + instanceDomain: z.string().url("Must be a valid URL").or(z.literal("")), + relay: z + .object( + { + id: z.string(), + name: z.string() + }, + { required_error: "Relay is required" } + ) + .nullable() + .refine((val) => val !== null, { message: "Relay is required" }) +}); + +const formSchemaWithIdentity = baseFormSchema.extend({ + identity: z + .object( + { + id: z.string(), + name: z.string() + }, + { required_error: "Identity is required" } + ) + .nullable() + .refine((val) => val !== null, { message: "Identity is required" }) +}); + +const formSchemaWithToken = baseFormSchema.extend({ + identityToken: z.string().min(1, "Token is required") +}); + +export const GatewayCliDeploymentMethod = () => { + const { protocol, hostname, port } = window.location; + const portSuffix = port && port !== "80" ? `:${port}` : ""; + const siteURL = `${protocol}//${hostname}${portSuffix}`; + + const navigate = useNavigate({ + from: ROUTE_PATHS.Organization.NetworkingPage.path + }); + + const [autogenerateToken, setAutogenerateToken] = useState(true); + const [step, setStep] = useState<"form" | "command">("form"); + const [name, setName] = useState(""); + const [instanceDomain, setInstanceDomain] = useState(siteURL); + const [relay, setRelay] = useState(null); + const [identity, setIdentity] = useState(null); + const [identityToken, setIdentityToken] = useState(""); + const [formErrors, setFormErrors] = useState([]); + + const errors = useMemo(() => { + const errorMap: Record = {}; + formErrors.forEach((issue) => { + if (issue.path.length > 0) { + errorMap[String(issue.path[0])] = issue.message; + } + }); + return errorMap; + }, [formErrors]); + + const { data: relays, isPending: isRelaysLoading } = useGetRelays(); + + const { currentOrg } = useOrganization(); + const organizationId = currentOrg?.id || ""; + + const { permission } = useOrgPermission(); + const canCreateToken = permission.can( + OrgPermissionIdentityActions.CreateToken, + OrgPermissionSubjects.Identity + ); + + const { data: identityMembershipOrgsData, isPending: isIdentitiesLoading } = + useGetIdentityMembershipOrgs({ + organizationId, + limit: 20000 + }); + const identityMembershipOrgs = identityMembershipOrgsData?.identityMemberships || []; + + const { mutateAsync: createToken, isPending: isCreatingToken } = + useCreateTokenIdentityTokenAuth(); + const { mutateAsync: addIdentityTokenAuth, isPending: isAddingTokenAuth } = + useAddIdentityTokenAuth(); + const { refetch } = useGetIdentityTokenAuth(identity?.id ?? ""); + + const handleGenerateCommand = async () => { + setFormErrors([]); + + if (canCreateToken && autogenerateToken) { + const validation = formSchemaWithIdentity.safeParse({ + name, + relay, + identity, + instanceDomain + }); + if (!validation.success) { + setFormErrors(validation.error.issues); + return; + } + + const validatedIdentity = validation.data.identity; + + try { + const { data: identityTokenAuth } = await refetch(); + if (!identityTokenAuth) { + await addIdentityTokenAuth({ + identityId: validatedIdentity.id, + organizationId, + accessTokenTTL: 2592000, + accessTokenMaxTTL: 2592000, + accessTokenNumUsesLimit: 0, + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + }); + createNotification({ + text: "Token authentication has been automatically enabled for the selected identity. By default, it is configured to allow all IP addresses with a default token TTL of 30 days. You can manage these settings in Access Control.", + type: "warning" + }); + } + + const token = await createToken({ + identityId: validatedIdentity.id, + name: `gateway token for ${name} (autogenerated)` + }); + setIdentityToken(token.accessToken); + createNotification({ + text: "Automatically generated a token for the selected identity.", + type: "info" + }); + setStep("command"); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to generate token for the selected identity", + type: "error" + }); + setIdentityToken(""); + } + } else { + const validation = formSchemaWithToken.safeParse({ + name, + relay, + identityToken, + instanceDomain + }); + if (!validation.success) { + setFormErrors(validation.error.issues); + return; + } + setStep("command"); + } + }; + + const command = useMemo(() => { + const domainFlag = instanceDomain ? ` --domain=${instanceDomain}` : ""; + return `infisical gateway start --name=${name} --relay=${ + relay?.name || "" + }${domainFlag} --token=${identityToken}`; + }, [name, relay, identityToken, instanceDomain]); + + if (step === "command") { + return ( + <> + +
+ + { + navigator.clipboard.writeText(command); + createNotification({ + text: "Command copied to clipboard", + type: "info" + }); + }} + className="w-10" + > + + +
+ + Install the Infisical CLI + + +
+ + + +
+ + ); + } + + return ( + <> + + setName(e.target.value)} + placeholder="Enter gateway name..." + isError={Boolean(errors.name)} + /> + {errors.name &&

{errors.name}

} + + + { + if ((newValue as SingleValue<{ id: string }>)?.id === "_create") { + navigate({ + search: (prev) => ({ ...prev, selectedTab: "relays", action: "deploy-relay" }) + }); + return; + } + + setRelay(newValue as SingleValue<{ id: string; name: string }>); + }} + isLoading={isRelaysLoading} + options={[ + { + id: "_create", + name: "Deploy New Relay" + }, + ...(relays || []) + ]} + placeholder="Select relay..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + components={{ Option: RelayOption }} + /> + {errors.relay &&

{errors.relay}

} + + + setInstanceDomain(e.target.value)} + placeholder="https://app.infisical.com" + isError={Boolean(errors.instanceDomain)} + /> + {errors.instanceDomain &&

{errors.instanceDomain}

} + + {canCreateToken && autogenerateToken ? ( + <> + + + setIdentity( + e as SingleValue<{ + id: string; + name: string; + }> + ) + } + isLoading={isIdentitiesLoading} + placeholder="Select identity..." + options={identityMembershipOrgs.map((membership) => membership.identity)} + getOptionValue={(option) => option.id} + getOptionLabel={(option) => option.name} + /> + {errors.identity &&

{errors.identity}

} + + ) : ( + <> + + setIdentityToken(e.target.value)} + placeholder="Enter identity token..." + isError={Boolean(errors.identityToken)} + /> + {errors.identityToken &&

{errors.identityToken}

} + + )} + + {canCreateToken && ( +
+ { + setAutogenerateToken(Boolean(e)); + }} + id="autogenerate-token" + className="mr-2" + > +
+ Automatically enable token auth and generate a token for identity + + Token authentication will be automatically enabled for the selected identity if + it isn't already configured. By default, it will be configured to allow all + IP addresses with a token TTL of 30 days. You can manage these settings in + Access Control. +
+
A token will automatically be generated to be used with the CLI command. + + } + > + +
+
+
+
+ )} + +
+ + + + +
+ + ); +}; diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeployModal.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeployModal.tsx new file mode 100644 index 000000000..170ea2eab --- /dev/null +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeployModal.tsx @@ -0,0 +1,45 @@ +import { useState } from "react"; + +import { Modal, ModalContent } from "@app/components/v2"; +import { GatewayDeploymentMethodSelect } from "@app/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeploymentMethodSelect"; + +import { GatewayCliDeploymentMethod } from "./GatewayCliDeploymentMethod"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; +}; + +export const GatewayDeploymentInfoMap = { + cli: { name: "CLI", image: "SSH.png", component: GatewayCliDeploymentMethod } +} as const; + +export type GatewayDeploymentMethod = keyof typeof GatewayDeploymentInfoMap; + +const Content = () => { + const [selectedMethod, setSelectedMethod] = useState(null); + + if (selectedMethod) { + const ComponentToRender = GatewayDeploymentInfoMap[selectedMethod]?.component; + if (ComponentToRender) { + return ; + } + } + + return ; +}; + +export const GatewayDeployModal = ({ isOpen, onOpenChange }: Props) => { + return ( + + + + + + ); +}; diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeploymentMethodSelect.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeploymentMethodSelect.tsx new file mode 100644 index 000000000..20a169c24 --- /dev/null +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeploymentMethodSelect.tsx @@ -0,0 +1,54 @@ +import { useMemo } from "react"; + +import { + GatewayDeploymentInfoMap, + GatewayDeploymentMethod +} from "@app/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeployModal"; + +type Props = { + onSelect: (method: GatewayDeploymentMethod) => void; +}; + +export const GatewayDeploymentMethodSelect = ({ onSelect }: Props) => { + const deploymentOptions = useMemo( + () => + (Object.keys(GatewayDeploymentInfoMap) as GatewayDeploymentMethod[]).map((method) => ({ + method, + name: GatewayDeploymentInfoMap[method].name, + image: GatewayDeploymentInfoMap[method].image + })), + [] + ); + + const handleResourceSelect = (method: GatewayDeploymentMethod) => { + onSelect(method); + }; + + return ( +
+ {deploymentOptions.map((option) => { + const { image, name, method } = option; + + return ( + + ); + })} +
+ ); +}; diff --git a/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/RelayOption.tsx b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/RelayOption.tsx new file mode 100644 index 000000000..90c9a7ebd --- /dev/null +++ b/frontend/src/pages/organization/NetworkingPage/components/GatewayTab/components/RelayOption.tsx @@ -0,0 +1,32 @@ +import { components, OptionProps } from "react-select"; +import { faCheckCircle } from "@fortawesome/free-regular-svg-icons"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +export const RelayOption = ({ + isSelected, + children, + ...props +}: OptionProps<{ id: string; name: string }>) => { + const isCreateOption = props.data.id === "_create"; + + return ( + +
+ {isCreateOption ? ( +
+ + Deploy New Relay +
+ ) : ( + <> +

{children}

+ {isSelected && ( + + )} + + )} +
+
+ ); +}; diff --git a/frontend/src/pages/organization/NetworkingPage/components/NetworkingTabGroup/NetworkingTabGroup.tsx b/frontend/src/pages/organization/NetworkingPage/components/NetworkingTabGroup/NetworkingTabGroup.tsx index 5aca54a00..b48b8b113 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/NetworkingTabGroup/NetworkingTabGroup.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/NetworkingTabGroup/NetworkingTabGroup.tsx @@ -1,15 +1,20 @@ -import { useState } from "react"; -import { useSearch } from "@tanstack/react-router"; +import { useNavigate, useSearch } from "@tanstack/react-router"; import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; +import { ROUTE_PATHS } from "@app/const/routes"; import { useOrganization } from "@app/context"; import { GatewayTab } from "../GatewayTab/GatewayTab"; import { RelayTab } from "../RelayTab/RelayTab"; export const NetworkingTabGroup = () => { - const search = useSearch({ - from: "/_authenticate/_inject-org-details/_org-layout/organization/networking/" + const navigate = useNavigate({ + from: ROUTE_PATHS.Organization.NetworkingPage.path + }); + const selectedTab = useSearch({ + from: ROUTE_PATHS.Organization.NetworkingPage.id, + select: (el) => el.selectedTab, + structuralSharing: true }); const tabs = [ @@ -17,12 +22,16 @@ export const NetworkingTabGroup = () => { { name: "Relays", key: "relays", component: RelayTab } ]; - const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key); + const handleTabChange = (tab: string) => { + navigate({ + search: { selectedTab: tab } + }); + }; const { isSubOrganization } = useOrganization(); return ( - + {tabs.map((tab) => ( diff --git a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/RelayTab.tsx b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/RelayTab.tsx index e525b765d..594c9a27c 100644 --- a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/RelayTab.tsx +++ b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/RelayTab.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { faArrowUpRightFromSquare, faBookOpen, @@ -7,15 +7,18 @@ import { faEllipsisV, faInfoCircle, faMagnifyingGlass, + faPlus, faSearch, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useNavigate, useSearch } from "@tanstack/react-router"; import { formatRelative } from "date-fns"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { + Button, DeleteActionModal, DropdownMenu, DropdownMenuContent, @@ -34,6 +37,7 @@ import { Tooltip, Tr } from "@app/components/v2"; +import { ROUTE_PATHS } from "@app/const/routes"; import { OrgPermissionSubjects, OrgRelayPermissionActions @@ -42,6 +46,8 @@ import { withPermission } from "@app/hoc"; import { usePopUp } from "@app/hooks"; import { useDeleteRelayById, useGetRelays } from "@app/hooks/api/relays"; +import { RelayDeployModal } from "./components/RelayDeployModal"; + const RelayHealthStatus = ({ heartbeat }: { heartbeat?: string }) => { const heartbeatDate = heartbeat ? new Date(heartbeat) : null; const now = new Date(); @@ -66,7 +72,29 @@ export const RelayTab = withPermission( const [search, setSearch] = useState(""); const { data: relays, isPending: isRelaysLoading } = useGetRelays(); - const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["deleteRelay"] as const); + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ + "deleteRelay", + "deployRelay" + ] as const); + + const action = useSearch({ + from: ROUTE_PATHS.Organization.NetworkingPage.id, + select: (s) => s.action + }); + + const navigate = useNavigate({ + from: ROUTE_PATHS.Organization.NetworkingPage.path + }); + + useEffect(() => { + if (action === "deploy-relay") { + handlePopUpOpen("deployRelay"); + navigate({ + search: (prev) => ({ ...prev, action: undefined }), + replace: true + }); + } + }, [action, handlePopUpOpen, navigate]); const deleteRelayById = useDeleteRelayById(); @@ -87,8 +115,8 @@ export const RelayTab = withPermission( return (
-
-
+
+

Relays

+
+

@@ -222,6 +258,10 @@ export const RelayTab = withPermission( deleteKey="confirm" onDeleteApproved={() => handleDeleteRelay()} /> + handlePopUpToggle("deployRelay", isOpen)} + />

diff --git a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayCliDeploymentMethod.tsx b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayCliDeploymentMethod.tsx new file mode 100644 index 000000000..1d89e9877 --- /dev/null +++ b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayCliDeploymentMethod.tsx @@ -0,0 +1,352 @@ +import { useMemo, useState } from "react"; +import { SingleValue } from "react-select"; +import { faCopy, faQuestionCircle, faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + Checkbox, + FilterableSelect, + FormLabel, + IconButton, + Input, + ModalClose, + Tooltip +} from "@app/components/v2"; +import { + OrgPermissionIdentityActions, + OrgPermissionSubjects, + useOrganization, + useOrgPermission +} from "@app/context"; +import { + useAddIdentityTokenAuth, + useCreateTokenIdentityTokenAuth, + useGetIdentityMembershipOrgs, + useGetIdentityTokenAuth +} from "@app/hooks/api"; +import { slugSchema } from "@app/lib/schemas"; + +const baseFormSchema = z.object({ + name: slugSchema({ field: "name" }), + host: z.string().min(1, "Host is required"), + instanceDomain: z.string().url("Must be a valid URL").or(z.literal("")) +}); + +const formSchemaWithIdentity = baseFormSchema.extend({ + identity: z + .object( + { + id: z.string(), + name: z.string() + }, + { required_error: "Identity is required" } + ) + .nullable() + .refine((val) => val !== null, { message: "Identity is required" }) +}); + +const formSchemaWithToken = baseFormSchema.extend({ + identityToken: z.string().min(1, "Token is required") +}); + +export const RelayCliDeploymentMethod = () => { + const { protocol, hostname, port } = window.location; + const portSuffix = port && port !== "80" ? `:${port}` : ""; + const siteURL = `${protocol}//${hostname}${portSuffix}`; + + const [autogenerateToken, setAutogenerateToken] = useState(true); + const [step, setStep] = useState<"form" | "command">("form"); + const [name, setName] = useState(""); + const [host, setHost] = useState(""); + + const [instanceDomain, setInstanceDomain] = useState(siteURL); + const [identity, setIdentity] = useState(null); + const [identityToken, setIdentityToken] = useState(""); + const [formErrors, setFormErrors] = useState([]); + + const errors = useMemo(() => { + const errorMap: Record = {}; + formErrors.forEach((issue) => { + if (issue.path.length > 0) { + errorMap[String(issue.path[0])] = issue.message; + } + }); + return errorMap; + }, [formErrors]); + + const { currentOrg } = useOrganization(); + const organizationId = currentOrg?.id || ""; + + const { permission } = useOrgPermission(); + const canCreateToken = permission.can( + OrgPermissionIdentityActions.CreateToken, + OrgPermissionSubjects.Identity + ); + + const { data: identityMembershipOrgsData, isPending: isIdentitiesLoading } = + useGetIdentityMembershipOrgs({ + organizationId, + limit: 20000 + }); + const identityMembershipOrgs = identityMembershipOrgsData?.identityMemberships || []; + + const { mutateAsync: createToken, isPending: isCreatingToken } = + useCreateTokenIdentityTokenAuth(); + const { mutateAsync: addIdentityTokenAuth, isPending: isAddingTokenAuth } = + useAddIdentityTokenAuth(); + const { refetch } = useGetIdentityTokenAuth(identity?.id ?? ""); + + const handleGenerateCommand = async () => { + setFormErrors([]); + + if (canCreateToken && autogenerateToken) { + const validation = formSchemaWithIdentity.safeParse({ name, host, instanceDomain, identity }); + if (!validation.success) { + setFormErrors(validation.error.issues); + return; + } + + const validatedIdentity = validation.data.identity; + + try { + const { data: identityTokenAuth } = await refetch(); + if (!identityTokenAuth) { + await addIdentityTokenAuth({ + identityId: validatedIdentity.id, + organizationId, + accessTokenTTL: 2592000, + accessTokenMaxTTL: 2592000, + accessTokenNumUsesLimit: 0, + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + }); + createNotification({ + text: "Token authentication has been automatically enabled for the selected identity. By default, it is configured to allow all IP addresses with a default token TTL of 30 days. You can manage these settings in Access Control.", + type: "warning" + }); + } + + const token = await createToken({ + identityId: validatedIdentity.id, + name: `relay token for ${name} (autogenerated)` + }); + setIdentityToken(token.accessToken); + createNotification({ + text: "Automatically generated a token for the selected identity.", + type: "info" + }); + setStep("command"); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to generate token for the selected identity", + type: "error" + }); + setIdentityToken(""); + } + } else { + const validation = formSchemaWithToken.safeParse({ + name, + host, + instanceDomain, + identityToken + }); + if (!validation.success) { + setFormErrors(validation.error.issues); + return; + } + setStep("command"); + } + }; + + const handleIdentityChange = ( + selectedIdentity: SingleValue<{ + id: string; + name: string; + }> + ) => { + setIdentity(selectedIdentity); + }; + + const command = useMemo(() => { + const domainFlag = instanceDomain ? ` --domain=${instanceDomain}` : ""; + return `infisical relay start --name=${name}${domainFlag} --host=${host} --token=${identityToken}`; + }, [name, instanceDomain, host, identityToken]); + + if (step === "command") { + return ( + <> + +
+ + { + navigator.clipboard.writeText(command); + createNotification({ + text: "Command copied to clipboard", + type: "info" + }); + }} + className="w-10" + > + + +
+ + Install the Infisical CLI + + +
+ + + +
+ + ); + } + + return ( + <> + + setName(e.target.value)} + placeholder="Enter relay name..." + isError={Boolean(errors.name)} + /> + {errors.name &&

{errors.name}

} + + + setHost(e.target.value)} + placeholder="0.0.0.0" + isError={Boolean(errors.host)} + /> + {errors.host &&

{errors.host}

} + + + setInstanceDomain(e.target.value)} + placeholder="https://app.infisical.com" + isError={Boolean(errors.instanceDomain)} + /> + {errors.instanceDomain &&

{errors.instanceDomain}

} + + {canCreateToken && autogenerateToken ? ( + <> + + + handleIdentityChange( + e as SingleValue<{ + id: string; + name: string; + }> + ) + } + isLoading={isIdentitiesLoading} + placeholder="Select identity..." + options={identityMembershipOrgs.map((membership) => membership.identity)} + getOptionValue={(option) => option.id} + getOptionLabel={(option) => option.name} + /> + {errors.identity &&

{errors.identity}

} + + ) : ( + <> + + setIdentityToken(e.target.value)} + placeholder="Enter identity token..." + isError={Boolean(errors.identityToken)} + /> + {errors.identityToken &&

{errors.identityToken}

} + + )} + + {canCreateToken && ( +
+ { + setAutogenerateToken(Boolean(e)); + }} + id="autogenerate-token" + className="mr-2" + > +
+ Automatically enable token auth and generate a token for identity + + Token authentication will be automatically enabled for the selected identity if + it isn't already configured. By default, it will be configured to allow all + IP addresses with a token TTL of 30 days. You can manage these settings in + Access Control. +
+
A token will automatically be generated to be used with the CLI command. + + } + > + +
+
+
+
+ )} + +
+ + + + +
+ + ); +}; diff --git a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeployModal.tsx b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeployModal.tsx new file mode 100644 index 000000000..496ecb762 --- /dev/null +++ b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeployModal.tsx @@ -0,0 +1,45 @@ +import { useState } from "react"; + +import { Modal, ModalContent } from "@app/components/v2"; +import { RelayDeploymentMethodSelect } from "@app/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeploymentMethodSelect"; + +import { RelayCliDeploymentMethod } from "./RelayCliDeploymentMethod"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; +}; + +export const RelayDeploymentInfoMap = { + cli: { name: "CLI", image: "SSH.png", component: RelayCliDeploymentMethod } +} as const; + +export type RelayDeploymentMethod = keyof typeof RelayDeploymentInfoMap; + +const Content = () => { + const [selectedMethod, setSelectedMethod] = useState(null); + + if (selectedMethod) { + const ComponentToRender = RelayDeploymentInfoMap[selectedMethod]?.component; + if (ComponentToRender) { + return ; + } + } + + return ; +}; + +export const RelayDeployModal = ({ isOpen, onOpenChange }: Props) => { + return ( + + + + + + ); +}; diff --git a/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeploymentMethodSelect.tsx b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeploymentMethodSelect.tsx new file mode 100644 index 000000000..cf7861a8b --- /dev/null +++ b/frontend/src/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeploymentMethodSelect.tsx @@ -0,0 +1,54 @@ +import { useMemo } from "react"; + +import { + RelayDeploymentInfoMap, + RelayDeploymentMethod +} from "@app/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeployModal"; + +type Props = { + onSelect: (method: RelayDeploymentMethod) => void; +}; + +export const RelayDeploymentMethodSelect = ({ onSelect }: Props) => { + const deploymentOptions = useMemo( + () => + (Object.keys(RelayDeploymentInfoMap) as RelayDeploymentMethod[]).map((method) => ({ + method, + name: RelayDeploymentInfoMap[method].name, + image: RelayDeploymentInfoMap[method].image + })), + [] + ); + + const handleResourceSelect = (method: RelayDeploymentMethod) => { + onSelect(method); + }; + + return ( +
+ {deploymentOptions.map((option) => { + const { image, name, method } = option; + + return ( + + ); + })} +
+ ); +}; diff --git a/frontend/src/pages/organization/NetworkingPage/route.tsx b/frontend/src/pages/organization/NetworkingPage/route.tsx index 906b12806..fb81e3725 100644 --- a/frontend/src/pages/organization/NetworkingPage/route.tsx +++ b/frontend/src/pages/organization/NetworkingPage/route.tsx @@ -5,16 +5,17 @@ import { z } from "zod"; import { NetworkingPage } from "./NetworkingPage"; const NetworkingPageQueryParams = z.object({ - selectedTab: z.string().catch("") + selectedTab: z.string().catch("gateways"), + action: z.string().optional() }); export const Route = createFileRoute( - "/_authenticate/_inject-org-details/_org-layout/organization/networking/" + "/_authenticate/_inject-org-details/_org-layout/organization/networking" )({ component: NetworkingPage, validateSearch: zodValidator(NetworkingPageQueryParams), search: { - middlewares: [stripSearchParams({ selectedTab: "" })] + middlewares: [stripSearchParams({ selectedTab: "gateways" })] }, context: () => ({ breadcrumbs: [ diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccessAccountModal.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccessAccountModal.tsx index 9049001e6..a1bf76888 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccessAccountModal.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccessAccountModal.tsx @@ -19,12 +19,53 @@ export const PamAccessAccountModal = ({ isOpen, onOpenChange, account }: Props) const isDurationValid = useMemo(() => duration && ms(duration || "1s") > 0, [duration]); + const cliDuration = useMemo(() => { + if (!duration) return duration; + + const unit = duration.replace(/[\d\s.-]/g, ""); + + const dayOrLargerUnits = [ + "d", + "day", + "days", + "w", + "week", + "weeks", + "y", + "yr", + "yrs", + "year", + "years" + ]; + + // ms library does not handle months (M) so we do it separately + if (unit === "M") { + const value = parseInt(duration, 10); + if (!Number.isNaN(value) && value > 0) { + const hours = value * 30 * 24; + return `${hours}h`; + } + } else if (dayOrLargerUnits.includes(unit.toLowerCase())) { + const valueInMs = ms(duration); + const oneHourInMs = 1000 * 60 * 60; + + if (typeof valueInMs === "number" && valueInMs > 0) { + const hours = Math.floor(valueInMs / oneHourInMs); + return `${hours}h`; + } + } + + return duration; + }, [duration]); + const command = useMemo( () => - account && account.resource.resourceType === PamResourceType.Postgres - ? `infisical pam db access-account ${account.id} --duration ${duration}` + account && + (account.resource.resourceType === PamResourceType.Postgres || + account.resource.resourceType === PamResourceType.MySQL) + ? `infisical pam db access-account ${account.id} --duration ${cliDuration}` : "", - [account, duration] + [account, cliDuration] ); if (!account) return null; @@ -48,7 +89,7 @@ export const PamAccessAccountModal = ({ isOpen, onOpenChange, account }: Props) />
- + Promise; +}; + +const formSchema = genericAccountFieldsSchema.extend({ + credentials: BaseSqlAccountSchema, + // We don't support rotation for now, just feed a false value to + // make the schema happy + rotationEnabled: z.boolean().default(false) +}); + +type FormData = z.infer; + +export const MySQLAccountForm = ({ account, onSubmit }: Props) => { + const isUpdate = Boolean(account); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: account + ? { + ...account, + credentials: { + ...account.credentials, + password: UNCHANGED_PASSWORD_SENTINEL + } + } + : undefined + }); + + const { + handleSubmit, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
{ + handleSubmit(onSubmit)(e); + }} + > + + +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PamAccountForm.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PamAccountForm.tsx index 8b553e656..b9599be2d 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PamAccountForm.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PamAccountForm.tsx @@ -8,6 +8,7 @@ import { import { DiscriminativePick } from "@app/types"; import { PamAccountHeader } from "../PamAccountHeader"; +import { MySQLAccountForm } from "./MySQLAccountForm"; import { PostgresAccountForm } from "./PostgresAccountForm"; type FormProps = { @@ -35,10 +36,7 @@ const CreateForm = ({ const createPamAccount = useCreatePamAccount(); const onSubmit = async ( - formData: DiscriminativePick< - TPamAccount, - "name" | "description" | "credentials" | "rotationEnabled" | "rotationIntervalSeconds" - > + formData: DiscriminativePick ) => { try { const account = await createPamAccount.mutateAsync({ @@ -72,6 +70,10 @@ const CreateForm = ({ resourceType={resourceType} /> ); + case PamResourceType.MySQL: + return ( + + ); default: throw new Error(`Unhandled resource: ${resourceType}`); } @@ -81,10 +83,7 @@ const UpdateForm = ({ account, onComplete }: UpdateFormProps) => { const updatePamAccount = useUpdatePamAccount(); const onSubmit = async ( - formData: DiscriminativePick< - TPamAccount, - "name" | "description" | "credentials" | "rotationEnabled" | "rotationIntervalSeconds" - > + formData: DiscriminativePick ) => { try { const updatedAccount = await updatePamAccount.mutateAsync({ @@ -110,6 +109,8 @@ const UpdateForm = ({ account, onComplete }: UpdateFormProps) => { switch (account.resource.resourceType) { case PamResourceType.Postgres: return ; + case PamResourceType.MySQL: + return ; default: throw new Error(`Unhandled resource: ${account.resource.resourceType}`); } diff --git a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx index 3e5994344..6d361877e 100644 --- a/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx +++ b/frontend/src/pages/pam/PamAccountsPage/components/PamAccountForm/PostgresAccountForm.tsx @@ -1,16 +1,21 @@ +import { zodResolver } from "@hookform/resolvers/zod"; import { useEffect, useState } from "react"; import { FormProvider, useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Button, ModalClose } from "@app/components/v2"; -import { PamResourceType, TPostgresAccount, useGetPamResourceById } from "@app/hooks/api/pam"; +import { + PamResourceType, + TPostgresAccount, + TPostgresResource, + useGetPamResourceById +} from "@app/hooks/api/pam"; import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants"; -import { BaseSqlAccountSchema } from "./shared/sql-account-schemas"; -import { SqlAccountFields } from "./shared/SqlAccountFields"; import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields"; import { RotateAccountFields, rotateAccountFieldsSchema } from "./RotateAccountFields"; +import { BaseSqlAccountSchema } from "./shared/sql-account-schemas"; +import { SqlAccountFields } from "./shared/SqlAccountFields"; type Props = { account?: TPostgresAccount; @@ -56,7 +61,9 @@ export const PostgresAccountForm = ({ account, resourceId, resourceType, onSubmi if (account) { setRotationCredentialsConfigured(account.resource.rotationCredentialsConfigured); } else { - setRotationCredentialsConfigured(!!resource?.rotationAccountCredentials); + setRotationCredentialsConfigured( + !!(resource as TPostgresResource)?.rotationAccountCredentials + ); } }, [account, resource]); diff --git a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/MySQLResourceForm.tsx b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/MySQLResourceForm.tsx new file mode 100644 index 000000000..b7c2996d3 --- /dev/null +++ b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/MySQLResourceForm.tsx @@ -0,0 +1,84 @@ +import { useState } from "react"; +import { FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { Button, ModalClose } from "@app/components/v2"; +import { PamResourceType, TMySQLResource } from "@app/hooks/api/pam"; + +import { BaseSqlResourceSchema } from "./shared/sql-resource-schemas"; +import { SqlResourceFields } from "./shared/SqlResourceFields"; +import { GenericResourceFields, genericResourceFieldsSchema } from "./GenericResourceFields"; + +type Props = { + resource?: TMySQLResource; + onSubmit: (formData: FormData) => Promise; +}; + +const formSchema = genericResourceFieldsSchema.extend({ + resourceType: z.literal(PamResourceType.MySQL), + connectionDetails: BaseSqlResourceSchema.extend({ + database: z.string().trim().optional().default("") + }) +}); + +type FormData = z.infer; + +export const MySQLResourceForm = ({ resource, onSubmit }: Props) => { + const isUpdate = Boolean(resource); + const [selectedTabIndex, setSelectedTabIndex] = useState(0); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: resource ?? { + resourceType: PamResourceType.MySQL, + connectionDetails: { + host: "", + port: 3306, + database: "", + sslEnabled: true, + sslRejectUnauthorized: true, + sslCertificate: undefined + } + } + }); + + const { + handleSubmit, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
{ + setSelectedTabIndex(0); + handleSubmit(onSubmit)(e); + }} + > + + +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PamResourceForm.tsx b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PamResourceForm.tsx index 8cfdc8582..2bc54e7cd 100644 --- a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PamResourceForm.tsx +++ b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/PamResourceForm.tsx @@ -9,6 +9,7 @@ import { import { DiscriminativePick } from "@app/types"; import { PamResourceHeader } from "../PamResourceHeader"; +import { MySQLResourceForm } from "./MySQLResourceForm"; import { PostgresResourceForm } from "./PostgresResourceForm"; type FormProps = { @@ -57,6 +58,8 @@ const CreateForm = ({ resourceType, onComplete, projectId }: CreateFormProps) => switch (resourceType) { case PamResourceType.Postgres: return ; + case PamResourceType.MySQL: + return ; default: throw new Error(`Unhandled resource: ${resourceType}`); } @@ -92,8 +95,10 @@ const UpdateForm = ({ resource, onComplete }: UpdateFormProps) => { switch (resource.resourceType) { case PamResourceType.Postgres: return ; + case PamResourceType.MySQL: + return ; default: - throw new Error(`Unhandled resource: ${resource.resourceType}`); + throw new Error(`Unhandled resource: ${(resource as any).resourceType}`); } }; diff --git a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/shared/SqlResourceFields.tsx b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/shared/SqlResourceFields.tsx index 888b62984..c3f3982ec 100644 --- a/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/shared/SqlResourceFields.tsx +++ b/frontend/src/pages/pam/PamResourcesPage/components/PamResourceForm/shared/SqlResourceFields.tsx @@ -110,7 +110,7 @@ export const SqlResourceFields = ({ setSelectedTabIndex, selectedTabIndex }: Pro errorText={error?.message} isError={Boolean(error?.message)} className={sslEnabled ? "" : "opacity-50"} - label="SSL Certificate" + label="Trusted CA SSL Certificate" isOptional >