UNPKG

@speckle/shared

Version:

Shared code between various Speckle JS packages

343 lines (315 loc) 10.5 kB
import { WorkspaceRoles } from '../../core/constants.js' import { WorkspaceLimits } from './limits.js' import { PaidWorkspacePlans, UnpaidWorkspacePlans, WorkspacePlanBillingIntervals, WorkspacePlans } from './plans.js' import type { MaybeNullOrUndefined } from '../../core/helpers/utilityTypes.js' import { FeatureFlags } from '../../environment/featureFlags.js' /** * WORKSPACE FEATURES */ export const WorkspacePlanFeatures = <const>{ // Core features pretty much available to everyone AutomateBeta: 'automateBeta', DomainDiscoverability: 'domainDiscoverability', // Optional/plan specific DomainSecurity: 'domainBasedSecurityPolicies', SSO: 'oidcSso', CustomDataRegion: 'workspaceDataRegionSpecificity', HideSpeckleBranding: 'hideSpeckleBranding', ExclusiveMembership: 'exclusiveMembership', EmbedPrivateProjects: 'embedPrivateProjects', SavedViews: 'savedViews' } export type WorkspacePlanFeatures = (typeof WorkspacePlanFeatures)[keyof typeof WorkspacePlanFeatures] // this const will be used as a bitwise flag for a per workspace feature access controller // IMPORTANT: always use powers of 2 as the value of the object // read more https://www.hendrik-erz.de/post/bitwise-flags-are-beautiful-and-heres-why // this will make its way to the pricing plan and info setup at some point // but for now its an internal only control export const WorkspaceFeatureFlags = <const>{ none: 0, dashboards: 1, accIntegration: 2, // High numbers for internal features presentations: 64 } export type WorkspaceFeatureFlags = (typeof WorkspaceFeatureFlags)[keyof typeof WorkspaceFeatureFlags] export const isWorkspaceFeatureFlagOn = ({ workspaceFeatureFlags, feature }: { workspaceFeatureFlags: number feature: WorkspaceFeatureFlags }): boolean => (workspaceFeatureFlags & feature) === feature export type WorkspaceFeatures = WorkspacePlanFeatures | WorkspaceFeatureFlags export const WorkspacePlanFeaturesMetadata = (<const>{ [WorkspacePlanFeatures.AutomateBeta]: { displayName: 'Automate beta access', description: 'Run custom automations on every new model version' }, [WorkspacePlanFeatures.DomainDiscoverability]: { displayName: 'Domain discoverability', description: 'Allow people to discover your workspace if they use a verified company email' }, [WorkspacePlanFeatures.DomainSecurity]: { displayName: 'Domain protection', description: 'Require workspace members to use a verified company email' }, [WorkspacePlanFeatures.SSO]: { displayName: 'Single Sign-On (SSO)', description: 'Require workspace members to authenticate with your SSO provider' }, [WorkspacePlanFeatures.CustomDataRegion]: { displayName: 'Custom data residency', description: 'Store your data in EU, UK, North America, or Asia Pacific' }, [WorkspacePlanFeatures.HideSpeckleBranding]: { displayName: 'Customised viewer', description: 'Hide the Speckle branding in embedded viewer' }, [WorkspacePlanFeatures.ExclusiveMembership]: { displayName: 'Exclusive workspace membership', description: 'Members of exclusive workspaces cannot join or create other workspaces' }, [WorkspacePlanFeatures.EmbedPrivateProjects]: { displayName: 'Embed private projects', description: 'Embed projects with visibility set to private or workspace' }, [WorkspacePlanFeatures.SavedViews]: { displayName: 'Saved views', description: 'Create and share saved views of your models' } }) satisfies Record< WorkspacePlanFeatures, { displayName: string description: string } > /** * PLAN CONFIG - PRICES & FEATURES */ export type WorkspacePlanPriceStructure = { [interval in WorkspacePlanBillingIntervals]: { [role in WorkspaceRoles]: number } } const unlimited: WorkspaceLimits = { projectCount: null, modelCount: null, versionsHistory: null, commentHistory: null } export type WorkspacePlanConfig<Plan extends WorkspacePlans = WorkspacePlans> = { plan: Plan features: readonly WorkspacePlanFeatures[] limits: WorkspaceLimits } const baseFeatures = [ WorkspacePlanFeatures.AutomateBeta, WorkspacePlanFeatures.DomainDiscoverability, WorkspacePlanFeatures.EmbedPrivateProjects ] as const export const WorkspacePaidPlanConfigs: (params: { featureFlags: Partial<FeatureFlags> | undefined }) => { [plan in PaidWorkspacePlans]: WorkspacePlanConfig<plan> } = (params) => { const finalBaseFeatures = [ ...baseFeatures, ...(params.featureFlags?.FF_SAVED_VIEWS_ENABLED ? [WorkspacePlanFeatures.SavedViews] : []) ] return { [PaidWorkspacePlans.Team]: { plan: PaidWorkspacePlans.Team, features: [...finalBaseFeatures], limits: { projectCount: 5, modelCount: 25, versionsHistory: { value: 30, unit: 'day' }, commentHistory: { value: 30, unit: 'day' } } }, [PaidWorkspacePlans.TeamUnlimited]: { plan: PaidWorkspacePlans.TeamUnlimited, features: [...finalBaseFeatures], limits: { projectCount: null, modelCount: null, versionsHistory: { value: 30, unit: 'day' }, commentHistory: { value: 30, unit: 'day' } } }, [PaidWorkspacePlans.Pro]: { plan: PaidWorkspacePlans.Pro, features: [ ...finalBaseFeatures, WorkspacePlanFeatures.DomainSecurity, WorkspacePlanFeatures.SSO, WorkspacePlanFeatures.CustomDataRegion, WorkspacePlanFeatures.HideSpeckleBranding ], limits: { projectCount: 10, modelCount: 50, versionsHistory: null, commentHistory: null } }, [PaidWorkspacePlans.ProUnlimited]: { plan: PaidWorkspacePlans.ProUnlimited, features: [ ...finalBaseFeatures, WorkspacePlanFeatures.DomainSecurity, WorkspacePlanFeatures.SSO, WorkspacePlanFeatures.CustomDataRegion, WorkspacePlanFeatures.HideSpeckleBranding ], limits: { projectCount: null, modelCount: null, versionsHistory: null, commentHistory: null } } } } export const WorkspaceUnpaidPlanConfigs: (params: { featureFlags: Partial<FeatureFlags> | undefined }) => { [plan in UnpaidWorkspacePlans]: WorkspacePlanConfig<plan> } = (params) => { const finalBaseFeatures = [ ...baseFeatures, ...(params.featureFlags?.FF_SAVED_VIEWS_ENABLED ? [WorkspacePlanFeatures.SavedViews] : []) ] return { [UnpaidWorkspacePlans.Enterprise]: { plan: UnpaidWorkspacePlans.Enterprise, features: [ ...finalBaseFeatures, WorkspacePlanFeatures.DomainSecurity, WorkspacePlanFeatures.SSO, WorkspacePlanFeatures.CustomDataRegion, WorkspacePlanFeatures.HideSpeckleBranding, WorkspacePlanFeatures.ExclusiveMembership ], limits: unlimited }, [UnpaidWorkspacePlans.Unlimited]: { plan: UnpaidWorkspacePlans.Unlimited, features: [ ...finalBaseFeatures, WorkspacePlanFeatures.DomainSecurity, WorkspacePlanFeatures.SSO, WorkspacePlanFeatures.CustomDataRegion, WorkspacePlanFeatures.HideSpeckleBranding, WorkspacePlanFeatures.ExclusiveMembership ], limits: unlimited }, [UnpaidWorkspacePlans.Academia]: { plan: UnpaidWorkspacePlans.Academia, features: [ ...finalBaseFeatures, WorkspacePlanFeatures.DomainSecurity, WorkspacePlanFeatures.SSO, WorkspacePlanFeatures.CustomDataRegion, WorkspacePlanFeatures.HideSpeckleBranding ], limits: unlimited }, [UnpaidWorkspacePlans.TeamUnlimitedInvoiced]: { ...WorkspacePaidPlanConfigs(params).teamUnlimited, plan: UnpaidWorkspacePlans.TeamUnlimitedInvoiced }, [UnpaidWorkspacePlans.ProUnlimitedInvoiced]: { ...WorkspacePaidPlanConfigs(params).proUnlimited, plan: UnpaidWorkspacePlans.ProUnlimitedInvoiced }, [UnpaidWorkspacePlans.Free]: { plan: UnpaidWorkspacePlans.Free, features: finalBaseFeatures, limits: { projectCount: 1, modelCount: 5, versionsHistory: { value: 7, unit: 'day' }, commentHistory: { value: 7, unit: 'day' } } } } } export const WorkspacePlanConfigs = (params: { featureFlags: Partial<FeatureFlags> | undefined }) => ({ ...WorkspacePaidPlanConfigs(params), ...WorkspaceUnpaidPlanConfigs(params) }) /** * Checks if a workspace exceeds its plan limits for projects and models */ export const workspaceExceedsPlanLimit = (params: { plan: MaybeNullOrUndefined<WorkspacePlans> projectCount: MaybeNullOrUndefined<number> modelCount: MaybeNullOrUndefined<number> featureFlags: Partial<FeatureFlags> | undefined }): boolean => { const { plan, projectCount, modelCount, featureFlags } = params if (!plan) return false const planConfig = WorkspacePlanConfigs({ featureFlags })[plan] if (!planConfig) return false const limits = planConfig.limits if (!limits.projectCount || !limits.modelCount) return false if (!projectCount || !modelCount) return false return projectCount > limits.projectCount || modelCount > limits.modelCount } /** * Checks if a workspace reached its plan limits for projects and models */ export const workspaceReachedPlanLimit = (params: { plan: MaybeNullOrUndefined<WorkspacePlans> projectCount: MaybeNullOrUndefined<number> modelCount: MaybeNullOrUndefined<number> featureFlags: Partial<FeatureFlags> | undefined }): boolean => { const { plan, projectCount, modelCount, featureFlags } = params if (!plan) return false const planConfig = WorkspacePlanConfigs({ featureFlags })[plan] if (!planConfig) return false const limits = planConfig.limits if (!limits.projectCount || !limits.modelCount) return false return projectCount === limits.projectCount || modelCount === limits.modelCount } export const workspacePlanHasAccessToFeature = ({ plan, feature, featureFlags }: { plan: WorkspacePlans feature: WorkspacePlanFeatures featureFlags: Partial<FeatureFlags> | undefined }): boolean => { const planConfig = WorkspacePlanConfigs({ featureFlags })[plan] const hasAccess = planConfig.features.includes(feature) return hasAccess } export const isPlanFeature = ( feature: WorkspaceFeatures ): feature is WorkspacePlanFeatures => { if (typeof feature === 'number') { return false } return Object.values(WorkspacePlanFeatures).includes(feature) }