diff --git a/apps/api/src/db/schema.ts b/apps/api/src/db/schema.ts index fbec510..efc4e93 100644 --- a/apps/api/src/db/schema.ts +++ b/apps/api/src/db/schema.ts @@ -195,9 +195,9 @@ export const licenses = sqliteTable("licenses", { .references(() => user.id, { onDelete: "cascade" }), licenseKey: text("license_key").unique().notNull(), - // Plan information - plan: text("plan", { enum: ["free", "solo", "pro", "team"] }) - .default("free") + // Plan information (v4.0: community, pro, team, sovereign + legacy: free, solo) + plan: text("plan", { enum: ["community", "pro", "team", "sovereign", "free", "solo"] }) + .default("community") .notNull(), planType: text("plan_type", { enum: ["free", "monthly", "annual", "lifetime"] }) .default("monthly") diff --git a/apps/api/src/features.ts b/apps/api/src/features.ts index 05f0db4..4f0c59e 100644 --- a/apps/api/src/features.ts +++ b/apps/api/src/features.ts @@ -1,11 +1,16 @@ /** - * RepliMap Feature Definitions + * RepliMap Feature Definitions v4.0 * - * Updated to include new features: - * - deps (formerly blast) - * - snapshot - * - audit_fix (remediation generator) - * - graph modes (full, security) + * Philosophy: "Gate Output, Not Input" + * - Unlimited scans for all tiers + * - Unlimited resources per scan + * - Charge when users export/download + * + * Tier Structure: + * - COMMUNITY ($0): Full visibility, JSON export with metadata + * - PRO ($29): Terraform/CSV export, API access + * - TEAM ($99): Drift alerts, compliance reports, CI/CD + * - SOVEREIGN ($2,500): SSO, signed reports, air-gap, white-labeling */ // ============================================================================= @@ -41,7 +46,7 @@ export enum Feature { SNAPSHOT = 'snapshot', SNAPSHOT_DIFF = 'snapshot_diff', - // Advanced analysis (PRO+/TEAM+) + // Advanced analysis DEPS = 'deps', DEPS_EXPORT = 'deps_export', @@ -59,16 +64,41 @@ export enum Feature { EXPORT_HTML = 'export_html', EXPORT_MARKDOWN = 'export_markdown', EXPORT_TERRAFORM = 'export_terraform', + EXPORT_CSV = 'export_csv', + EXPORT_PDF = 'export_pdf', + + // Compliance features + COMPLIANCE_CIS = 'compliance_cis', + COMPLIANCE_SOC2 = 'compliance_soc2', + COMPLIANCE_APRA = 'compliance_apra', + COMPLIANCE_DORA = 'compliance_dora', + COMPLIANCE_ESSENTIAL8 = 'compliance_essential8', + COMPLIANCE_CUSTOM = 'compliance_custom', + + // Enterprise features + REPORT_SIGNATURE = 'report_signature', + TAMPER_EVIDENT_AUDIT = 'tamper_evident_audit', + AIR_GAP_DEPLOYMENT = 'air_gap_deployment', + WHITE_LABELING = 'white_labeling', + DEDICATED_SUPPORT = 'dedicated_support', } export enum Plan { - FREE = 'free', - SOLO = 'solo', + COMMUNITY = 'community', PRO = 'pro', TEAM = 'team', - ENTERPRISE = 'enterprise', + SOVEREIGN = 'sovereign', } +// Legacy plan names for backward compatibility +export type LegacyPlan = 'free' | 'solo' | 'enterprise'; + +export const LEGACY_PLAN_MIGRATIONS: Record = { + free: Plan.COMMUNITY, + solo: Plan.PRO, + enterprise: Plan.SOVEREIGN, +}; + // ============================================================================= // Feature Access Matrix by Plan // ============================================================================= @@ -76,50 +106,30 @@ export enum Plan { /** * Feature access matrix by plan * - * Aligned with replimap-plan-feature-gate-prompt.md + * v4.0 Philosophy: "Gate Output, Not Input" * - * Key decisions for NEW features: - * - AUDIT_FIX: SOLO+ (enhances audit, key Solo differentiator) - * - SNAPSHOT: SOLO+ (alternative to drift for non-TF users) - * - DEPS: TEAM+ (same as original BLAST - high-value feature) - * - GRAPH_FULL/SECURITY: SOLO+ (premium graph modes) + * Key decisions: + * - COMMUNITY: Can VIEW everything, but exports are limited + * - PRO: Unlocks Terraform/CSV export, API access, full audit + * - TEAM: Unlocks drift alerts, compliance reports, CI/CD + * - SOVEREIGN: Unlocks SSO, signed reports, air-gap, white-labeling */ export const PLAN_FEATURES: Record = { - // FREE ($0/mo) - Experience value, limit output - [Plan.FREE]: [ + // COMMUNITY ($0/mo) - Full visibility, gated output + [Plan.COMMUNITY]: [ Feature.SCAN, + Feature.SCAN_UNLIMITED_FREQUENCY, // v4.0: Unlimited scans for all! Feature.GRAPH, Feature.AUDIT, Feature.CLONE_GENERATE, Feature.EXPORT_JSON, Feature.SNAPSHOT, Feature.SNAPSHOT_DIFF, + Feature.COST, // v4.0: Can VIEW cost analysis + Feature.DEPS, // v4.0: Can VIEW dependency graph ], - // SOLO ($29/mo, $199/year) - Full individual access - [Plan.SOLO]: [ - Feature.SCAN, - Feature.SCAN_UNLIMITED_FREQUENCY, - Feature.GRAPH, - Feature.GRAPH_FULL, - Feature.GRAPH_SECURITY, - Feature.GRAPH_EXPORT_NO_WATERMARK, - Feature.AUDIT, - Feature.AUDIT_FULL_FINDINGS, - Feature.AUDIT_FIX, - Feature.AUDIT_REPORT_EXPORT, - Feature.CLONE_GENERATE, - Feature.CLONE_DOWNLOAD, - Feature.CLONE_FULL_PREVIEW, - Feature.SNAPSHOT, - Feature.SNAPSHOT_DIFF, - Feature.EXPORT_JSON, - Feature.EXPORT_HTML, - Feature.EXPORT_MARKDOWN, - Feature.EXPORT_TERRAFORM, - ], - - // PRO ($79/mo, $599/year) - CI/CD and advanced analysis + // PRO ($29/mo) - Export your infrastructure as code [Plan.PRO]: [ Feature.SCAN, Feature.SCAN_UNLIMITED_FREQUENCY, @@ -131,22 +141,24 @@ export const PLAN_FEATURES: Record = { Feature.AUDIT_FULL_FINDINGS, Feature.AUDIT_FIX, Feature.AUDIT_REPORT_EXPORT, - Feature.AUDIT_CI_MODE, Feature.CLONE_GENERATE, Feature.CLONE_DOWNLOAD, Feature.CLONE_FULL_PREVIEW, Feature.SNAPSHOT, Feature.SNAPSHOT_DIFF, - Feature.DRIFT, + Feature.DEPS, + Feature.DEPS_EXPORT, Feature.COST, + Feature.COST_EXPORT, Feature.MULTI_ACCOUNT, Feature.EXPORT_JSON, Feature.EXPORT_HTML, Feature.EXPORT_MARKDOWN, Feature.EXPORT_TERRAFORM, + Feature.EXPORT_CSV, ], - // TEAM ($149/mo, $1,199/year) - Full platform + // TEAM ($99/mo) - Continuous compliance for your organization [Plan.TEAM]: [ Feature.SCAN, Feature.SCAN_UNLIMITED_FREQUENCY, @@ -167,21 +179,25 @@ export const PLAN_FEATURES: Record = { Feature.DRIFT, Feature.DRIFT_WATCH, Feature.DRIFT_ALERTS, + Feature.DEPS, + Feature.DEPS_EXPORT, Feature.COST, Feature.COST_EXPORT, Feature.MULTI_ACCOUNT, - Feature.DEPS, - Feature.DEPS_EXPORT, Feature.WEB_DASHBOARD, Feature.TEAM_COLLABORATION, + Feature.COMPLIANCE_CIS, + Feature.COMPLIANCE_SOC2, Feature.EXPORT_JSON, Feature.EXPORT_HTML, Feature.EXPORT_MARKDOWN, Feature.EXPORT_TERRAFORM, + Feature.EXPORT_CSV, + Feature.EXPORT_PDF, ], - // ENTERPRISE ($399/mo, $3,999/year) - Everything - [Plan.ENTERPRISE]: Object.values(Feature) as Feature[], + // SOVEREIGN ($2,500/mo) - Data sovereignty for regulated industries + [Plan.SOVEREIGN]: Object.values(Feature) as Feature[], }; // ============================================================================= @@ -191,57 +207,35 @@ export const PLAN_FEATURES: Record = { /** * Usage limits by plan (per month) * - * Core Philosophy: - * - SCAN: Unlimited resources, limit FREQUENCY only (not resource count!) - * - GRAPH: Free to view, watermark on export for FREE - * - CLONE: Generate all, block DOWNLOAD for FREE - * - AUDIT: Scan all, limit VISIBLE findings for FREE - * - DRIFT: PRO+ feature (not in FREE or SOLO!) - * - COST: PRO+ feature - * - DEPS/BLAST: TEAM+ feature + * v4.0 Philosophy: + * - SCAN: Unlimited frequency and resources for ALL tiers + * - GRAPH: Free to view, watermark on export for COMMUNITY + * - CLONE: Generate all, block DOWNLOAD for COMMUNITY + * - AUDIT: Scan all, limit VISIBLE findings for COMMUNITY */ export const PLAN_LIMITS: Record> = { - // FREE TIER ($0/mo) - [Plan.FREE]: { - scan_count: 3, - resources_per_scan: -1, + // COMMUNITY TIER ($0/mo) - Unlimited scans, gated exports + [Plan.COMMUNITY]: { + scan_count: -1, // v4.0: UNLIMITED scans + resources_per_scan: -1, // v4.0: UNLIMITED resources graph_count: -1, clone_count: -1, - clone_preview_lines: 100, - clone_download: 0, + clone_preview_lines: 100, // Limited preview + clone_download: 0, // No download audit_count: -1, - audit_visible_findings: 3, + audit_visible_findings: 3, // See 3 findings, upgrade for more audit_fix_count: 0, - snapshot_count: 1, - snapshot_diff_count: 1, + snapshot_count: 3, + snapshot_diff_count: 3, drift_count: 0, - cost_count: 0, - deps_count: 0, + cost_count: -1, // v4.0: Can VIEW cost + deps_count: -1, // v4.0: Can VIEW deps aws_accounts: 1, machines: 1, + history_retention_days: 7, }, - // SOLO TIER ($29/mo, $199/year) - [Plan.SOLO]: { - scan_count: -1, - resources_per_scan: -1, - graph_count: -1, - clone_count: -1, - clone_preview_lines: -1, - clone_download: 1, - audit_count: -1, - audit_visible_findings: -1, - audit_fix_count: -1, - snapshot_count: -1, - snapshot_diff_count: -1, - drift_count: 0, - cost_count: 0, - deps_count: 0, - aws_accounts: 1, - machines: 2, - }, - - // PRO TIER ($79/mo, $599/year) + // PRO TIER ($29/mo) [Plan.PRO]: { scan_count: -1, resources_per_scan: -1, @@ -254,14 +248,15 @@ export const PLAN_LIMITS: Record> = { audit_fix_count: -1, snapshot_count: -1, snapshot_diff_count: -1, - drift_count: -1, + drift_count: 0, // No drift detection in PRO cost_count: -1, - deps_count: 0, + deps_count: -1, aws_accounts: 3, - machines: 3, + machines: 2, + history_retention_days: 90, }, - // TEAM TIER ($149/mo, $1,199/year) + // TEAM TIER ($99/mo) [Plan.TEAM]: { scan_count: -1, resources_per_scan: -1, @@ -280,10 +275,11 @@ export const PLAN_LIMITS: Record> = { aws_accounts: 10, machines: 10, team_members: 5, + history_retention_days: 365, }, - // ENTERPRISE TIER ($399/mo, $3,999/year) - [Plan.ENTERPRISE]: { + // SOVEREIGN TIER ($2,500/mo) + [Plan.SOVEREIGN]: { scan_count: -1, resources_per_scan: -1, graph_count: -1, @@ -301,6 +297,7 @@ export const PLAN_LIMITS: Record> = { aws_accounts: -1, machines: -1, team_members: -1, + history_retention_days: -1, }, }; @@ -321,116 +318,182 @@ export const FEATURE_METADATA: Partial> = { [Feature.SCAN]: { name: 'Infrastructure Scan', description: 'Scan AWS resources in a region or VPC', - tier: Plan.FREE, + tier: Plan.COMMUNITY, }, [Feature.GRAPH]: { name: 'Graph Visualization', description: 'Generate infrastructure dependency graphs', - tier: Plan.FREE, + tier: Plan.COMMUNITY, }, [Feature.GRAPH_FULL]: { name: 'Full Graph Mode', description: 'Show all resources without simplification (--all)', - tier: Plan.SOLO, - isNew: true, + tier: Plan.PRO, }, [Feature.GRAPH_SECURITY]: { name: 'Security Graph Mode', description: 'Security-focused view with SG rules (--security)', - tier: Plan.SOLO, - isNew: true, + tier: Plan.PRO, }, [Feature.CLONE_GENERATE]: { name: 'Infrastructure Cloning', description: 'Generate Terraform code to clone infrastructure', - tier: Plan.FREE, + tier: Plan.COMMUNITY, }, [Feature.CLONE_DOWNLOAD]: { name: 'Clone Download', description: 'Download generated Terraform code', - tier: Plan.SOLO, + tier: Plan.PRO, }, [Feature.AUDIT]: { name: 'Security Audit', description: 'Scan for security misconfigurations', - tier: Plan.FREE, + tier: Plan.COMMUNITY, }, [Feature.AUDIT_FIX]: { name: 'Audit Remediation', description: 'Auto-generate Terraform code to fix issues (--fix)', - tier: Plan.SOLO, - isNew: true, + tier: Plan.PRO, }, [Feature.AUDIT_CI_MODE]: { name: 'Audit CI Mode', description: 'Use --fail-on-high in CI/CD pipelines', - tier: Plan.PRO, + tier: Plan.TEAM, }, [Feature.DRIFT]: { name: 'Drift Detection', description: 'Compare AWS state vs Terraform state', - tier: Plan.PRO, + tier: Plan.TEAM, }, [Feature.DRIFT_WATCH]: { name: 'Drift Watch Mode', - description: 'Continuous drift monitoring with alerts', + description: 'Continuous drift monitoring', + tier: Plan.TEAM, + }, + [Feature.DRIFT_ALERTS]: { + name: 'Drift Alerts', + description: 'Slack/Teams/Webhook notifications for drift', tier: Plan.TEAM, }, [Feature.SNAPSHOT]: { name: 'Infrastructure Snapshot', - description: 'Save infrastructure state for comparison (no Terraform needed)', - tier: Plan.SOLO, - isNew: true, + description: 'Save infrastructure state for comparison', + tier: Plan.COMMUNITY, }, [Feature.SNAPSHOT_DIFF]: { name: 'Snapshot Comparison', description: 'Compare snapshots to detect changes over time', - tier: Plan.SOLO, - isNew: true, + tier: Plan.COMMUNITY, }, [Feature.DEPS]: { name: 'Dependency Explorer', - description: 'Explore resource dependencies and potential impact of changes', - tier: Plan.TEAM, - isRenamed: true, - previousName: 'Blast Radius Analyzer', + description: 'Explore resource dependencies and blast radius', + tier: Plan.COMMUNITY, // v4.0: VIEW is free }, [Feature.DEPS_EXPORT]: { name: 'Dependency Export', description: 'Export dependency analysis reports', - tier: Plan.TEAM, - isRenamed: true, - previousName: 'Blast Radius Export', + tier: Plan.PRO, }, [Feature.COST]: { name: 'Cost Estimation', - description: 'Estimate infrastructure costs (±20% accuracy)', - tier: Plan.PRO, + description: 'Estimate infrastructure costs', + tier: Plan.COMMUNITY, // v4.0: VIEW is free }, [Feature.COST_EXPORT]: { name: 'Cost Export', description: 'Export cost estimation reports', - tier: Plan.TEAM, + tier: Plan.PRO, }, [Feature.EXPORT_JSON]: { name: 'JSON Export', description: 'Export data as JSON', - tier: Plan.FREE, + tier: Plan.COMMUNITY, }, [Feature.EXPORT_HTML]: { name: 'HTML Export', description: 'Export reports as HTML', - tier: Plan.SOLO, + tier: Plan.PRO, }, [Feature.EXPORT_MARKDOWN]: { name: 'Markdown Export', description: 'Export reports as Markdown', - tier: Plan.SOLO, + tier: Plan.PRO, }, [Feature.EXPORT_TERRAFORM]: { name: 'Terraform Export', description: 'Export as Terraform code', - tier: Plan.SOLO, + tier: Plan.PRO, + }, + [Feature.EXPORT_CSV]: { + name: 'CSV Export', + description: 'Export data as CSV for spreadsheets', + tier: Plan.PRO, + }, + [Feature.EXPORT_PDF]: { + name: 'PDF Export', + description: 'Export reports as PDF', + tier: Plan.TEAM, + }, + [Feature.COMPLIANCE_CIS]: { + name: 'CIS Benchmark', + description: 'CIS Benchmark compliance reports', + tier: Plan.TEAM, + }, + [Feature.COMPLIANCE_SOC2]: { + name: 'SOC2 Compliance', + description: 'SOC2 compliance mapping', + tier: Plan.TEAM, + }, + [Feature.COMPLIANCE_APRA]: { + name: 'APRA CPS 234', + description: 'Australian prudential regulation compliance', + tier: Plan.SOVEREIGN, + }, + [Feature.COMPLIANCE_DORA]: { + name: 'DORA Compliance', + description: 'EU Digital Operational Resilience Act', + tier: Plan.SOVEREIGN, + }, + [Feature.COMPLIANCE_ESSENTIAL8]: { + name: 'Essential Eight', + description: 'Australian Cyber Security Centre maturity assessment', + tier: Plan.SOVEREIGN, + }, + [Feature.COMPLIANCE_CUSTOM]: { + name: 'Custom Compliance', + description: 'Create custom compliance frameworks', + tier: Plan.SOVEREIGN, + }, + [Feature.SSO]: { + name: 'Single Sign-On', + description: 'SAML/OIDC integration', + tier: Plan.SOVEREIGN, + }, + [Feature.REPORT_SIGNATURE]: { + name: 'Signed Reports', + description: 'SHA256 digital signatures for audit evidence', + tier: Plan.SOVEREIGN, + }, + [Feature.TAMPER_EVIDENT_AUDIT]: { + name: 'Tamper-Evident Audit', + description: 'Immutable audit trail for compliance', + tier: Plan.SOVEREIGN, + }, + [Feature.AIR_GAP_DEPLOYMENT]: { + name: 'Air-Gap Deployment', + description: 'Deploy in isolated networks with zero external connections', + tier: Plan.SOVEREIGN, + }, + [Feature.WHITE_LABELING]: { + name: 'White-Labeling', + description: 'Remove RepliMap branding for client deliverables', + tier: Plan.SOVEREIGN, + }, + [Feature.DEDICATED_SUPPORT]: { + name: 'Dedicated Support', + description: 'Named account manager', + tier: Plan.SOVEREIGN, }, }; @@ -451,7 +514,7 @@ export function planHasFeature(plan: Plan, feature: Feature): boolean { */ export function getRequiredPlan(feature: Feature): Plan { const metadata = FEATURE_METADATA[feature]; - return metadata?.tier ?? Plan.ENTERPRISE; + return metadata?.tier ?? Plan.SOVEREIGN; } /** @@ -469,6 +532,32 @@ export function isUnlimited(limit: number): boolean { return limit === -1; } +/** + * Normalize a plan name, converting legacy names to v4.0 names + */ +export function normalizePlan(plan: string): Plan { + const lower = plan.toLowerCase(); + + // Check v4.0 plan names + if (Object.values(Plan).includes(lower as Plan)) { + return lower as Plan; + } + + // Check legacy plan names + if (lower in LEGACY_PLAN_MIGRATIONS) { + return LEGACY_PLAN_MIGRATIONS[lower as LegacyPlan]; + } + + return Plan.COMMUNITY; // Default to community for unknown plans +} + +/** + * Check if a plan name is a legacy plan + */ +export function isLegacyPlan(plan: string): boolean { + return plan.toLowerCase() in LEGACY_PLAN_MIGRATIONS; +} + /** * Get all new features */ @@ -499,9 +588,21 @@ export interface FeatureFlagsType { graph_security: boolean; drift: boolean; drift_watch: boolean; + drift_alerts: boolean; cost: boolean; clone_download: boolean; audit_ci_mode: boolean; + export_terraform: boolean; + export_csv: boolean; + export_pdf: boolean; + compliance_cis: boolean; + compliance_soc2: boolean; + compliance_apra: boolean; + compliance_dora: boolean; + sso: boolean; + report_signature: boolean; + air_gap: boolean; + white_labeling: boolean; } /** @@ -518,8 +619,58 @@ export function getFeatureFlags(plan: Plan): FeatureFlagsType { graph_security: features.includes(Feature.GRAPH_SECURITY), drift: features.includes(Feature.DRIFT), drift_watch: features.includes(Feature.DRIFT_WATCH), + drift_alerts: features.includes(Feature.DRIFT_ALERTS), cost: features.includes(Feature.COST), clone_download: features.includes(Feature.CLONE_DOWNLOAD), audit_ci_mode: features.includes(Feature.AUDIT_CI_MODE), + export_terraform: features.includes(Feature.EXPORT_TERRAFORM), + export_csv: features.includes(Feature.EXPORT_CSV), + export_pdf: features.includes(Feature.EXPORT_PDF), + compliance_cis: features.includes(Feature.COMPLIANCE_CIS), + compliance_soc2: features.includes(Feature.COMPLIANCE_SOC2), + compliance_apra: features.includes(Feature.COMPLIANCE_APRA), + compliance_dora: features.includes(Feature.COMPLIANCE_DORA), + sso: features.includes(Feature.SSO), + report_signature: features.includes(Feature.REPORT_SIGNATURE), + air_gap: features.includes(Feature.AIR_GAP_DEPLOYMENT), + white_labeling: features.includes(Feature.WHITE_LABELING), + }; +} + +// ============================================================================= +// Plan Comparison +// ============================================================================= + +export const PLAN_RANK: Record = { + // v4.0 plans + [Plan.COMMUNITY]: 0, + [Plan.PRO]: 1, + [Plan.TEAM]: 2, + [Plan.SOVEREIGN]: 3, + // Legacy plans + free: 0, + solo: 1, + enterprise: 3, +}; + +export function isPlanUpgrade(from: string, to: string): boolean { + const fromPlan = normalizePlan(from); + const toPlan = normalizePlan(to); + return PLAN_RANK[toPlan] > PLAN_RANK[fromPlan]; +} + +export function isPlanDowngrade(from: string, to: string): boolean { + const fromPlan = normalizePlan(from); + const toPlan = normalizePlan(to); + return PLAN_RANK[toPlan] < PLAN_RANK[fromPlan]; +} + +export function getUpgradePath(currentPlan: Plan): Plan | null { + const upgradePaths: Record = { + [Plan.COMMUNITY]: Plan.PRO, + [Plan.PRO]: Plan.TEAM, + [Plan.TEAM]: Plan.SOVEREIGN, + [Plan.SOVEREIGN]: null, }; + return upgradePaths[currentPlan]; } diff --git a/apps/api/src/handlers/activate-license.ts b/apps/api/src/handlers/activate-license.ts index a1888ff..b09086b 100644 --- a/apps/api/src/handlers/activate-license.ts +++ b/apps/api/src/handlers/activate-license.ts @@ -76,7 +76,7 @@ export async function handleActivateLicense( } const plan = license.plan as PlanType; - const features = PLAN_FEATURES[plan] ?? PLAN_FEATURES.free; + const features = PLAN_FEATURES[plan] ?? PLAN_FEATURES.community; // Check license status if (license.status === 'expired' || license.status === 'revoked') { diff --git a/apps/api/src/handlers/admin.ts b/apps/api/src/handlers/admin.ts index defb039..3b58fce 100644 --- a/apps/api/src/handlers/admin.ts +++ b/apps/api/src/handlers/admin.ts @@ -84,7 +84,7 @@ export async function handleCreateLicense( } // Validate plan - const plan = (body.plan || 'free') as PlanType; + const plan = (body.plan || 'community') as PlanType; if (!PLAN_FEATURES[plan]) { throw Errors.invalidRequest(`Invalid plan. Must be one of: ${Object.keys(PLAN_FEATURES).join(', ')}`); } @@ -217,7 +217,7 @@ export async function handleGetLicense( throw Errors.licenseNotFound(); } - const features = PLAN_FEATURES[license.plan as PlanType] ?? PLAN_FEATURES.free; + const features = PLAN_FEATURES[license.plan as PlanType] ?? PLAN_FEATURES.community; return new Response(JSON.stringify({ license_key: license.licenseKey, diff --git a/apps/api/src/handlers/aws-accounts.ts b/apps/api/src/handlers/aws-accounts.ts index de346b9..a840a0a 100644 --- a/apps/api/src/handlers/aws-accounts.ts +++ b/apps/api/src/handlers/aws-accounts.ts @@ -118,7 +118,7 @@ export async function handleTrackAwsAccount( // Get plan limits const plan = license.plan as PlanType; - const features = PLAN_FEATURES[plan] ?? PLAN_FEATURES.free; + const features = PLAN_FEATURES[plan] ?? PLAN_FEATURES.community; const maxAccounts = features.aws_accounts; // Get current count @@ -197,7 +197,7 @@ export async function handleGetAwsAccounts( // Get plan limits const plan = license.plan as PlanType; - const features = PLAN_FEATURES[plan] ?? PLAN_FEATURES.free; + const features = PLAN_FEATURES[plan] ?? PLAN_FEATURES.community; const response: GetAwsAccountsResponse = { accounts: accounts.map((acc) => ({ diff --git a/apps/api/src/handlers/features.ts b/apps/api/src/handlers/features.ts index 66ab18e..8a15f8d 100644 --- a/apps/api/src/handlers/features.ts +++ b/apps/api/src/handlers/features.ts @@ -93,7 +93,7 @@ export async function handleGetFeatures( try { // Check if license key provided const licenseKey = request.headers.get('X-License-Key'); - let plan: Plan = Plan.FREE; + let plan: Plan = Plan.COMMUNITY; if (licenseKey) { try { @@ -267,7 +267,7 @@ export async function handleGetFeatureFlags( try { const licenseKey = request.headers.get('X-License-Key'); - let plan: Plan = Plan.FREE; + let plan: Plan = Plan.COMMUNITY; if (licenseKey) { try { @@ -278,7 +278,7 @@ export async function handleGetFeatureFlags( plan = license.plan as Plan; } } catch { - // Invalid license key - default to FREE + // Invalid license key - default to COMMUNITY } } diff --git a/apps/api/src/handlers/rightsizer.ts b/apps/api/src/handlers/rightsizer.ts index e3d25f0..7114cd7 100644 --- a/apps/api/src/handlers/rightsizer.ts +++ b/apps/api/src/handlers/rightsizer.ts @@ -423,18 +423,18 @@ export async function handleRightSizerSuggestions( throw Errors.licenseRevoked(); } - // Check if plan includes rightsizer feature (Solo+) + // Check if plan includes rightsizer feature (Pro+) const plan = license.plan as Plan; - const allowedPlans: Plan[] = [Plan.SOLO, Plan.PRO, Plan.TEAM, Plan.ENTERPRISE]; + const allowedPlans: Plan[] = [Plan.PRO, Plan.TEAM, Plan.SOVEREIGN]; if (!allowedPlans.includes(plan)) { return new Response( JSON.stringify({ success: false, error: 'UPGRADE_REQUIRED', - message: 'Right-Sizer requires Solo plan or higher', + message: 'Right-Sizer requires Pro plan or higher', current_plan: plan, - required_plan: 'solo', + required_plan: 'pro', upgrade_url: 'https://replimap.dev/pricing', }), { diff --git a/apps/api/src/handlers/stripe-webhook.ts b/apps/api/src/handlers/stripe-webhook.ts index 5022964..4a51e00 100644 --- a/apps/api/src/handlers/stripe-webhook.ts +++ b/apps/api/src/handlers/stripe-webhook.ts @@ -222,7 +222,7 @@ async function createLifetimeLicense( // Resolve plan from price mapping const priceMapping = getStripePriceMapping(env); - let plan: 'free' | 'solo' | 'pro' | 'team' = 'solo'; + let plan: 'community' | 'pro' | 'team' | 'sovereign' = 'pro'; let priceId: string | null = null; // Try to get price from metadata first diff --git a/apps/api/src/handlers/usage.ts b/apps/api/src/handlers/usage.ts index df446fa..cb52c7f 100644 --- a/apps/api/src/handlers/usage.ts +++ b/apps/api/src/handlers/usage.ts @@ -158,7 +158,7 @@ export async function handleSyncUsage( // Return current usage without updating const currentUsage = await getUsageForPeriod(db, license.id, period); const plan = license.plan as PlanType; - const features = PLAN_FEATURES[plan] ?? PLAN_FEATURES.free; + const features = PLAN_FEATURES[plan] ?? PLAN_FEATURES.community; return new Response(JSON.stringify({ synced: true, @@ -183,7 +183,7 @@ export async function handleSyncUsage( // Get updated usage const currentUsage = await getUsageForPeriod(db, license.id, period); const plan = license.plan as PlanType; - const features = PLAN_FEATURES[plan] ?? PLAN_FEATURES.free; + const features = PLAN_FEATURES[plan] ?? PLAN_FEATURES.community; const response: SyncUsageResponse = { synced: true, @@ -241,7 +241,7 @@ export async function handleGetUsage( // Get usage const usage = await getUsageForPeriod(db, license.id, period); const plan = license.plan as PlanType; - const features = PLAN_FEATURES[plan] ?? PLAN_FEATURES.free; + const features = PLAN_FEATURES[plan] ?? PLAN_FEATURES.community; return new Response(JSON.stringify({ period, @@ -363,7 +363,7 @@ export async function handleCheckQuota( } const plan = license.plan as PlanType; - const features = PLAN_FEATURES[plan] ?? PLAN_FEATURES.free; + const features = PLAN_FEATURES[plan] ?? PLAN_FEATURES.community; let current = 0; let limit: number | null = null; @@ -677,7 +677,7 @@ async function checkEventLimit( }; } - const limits = PLAN_LIMITS[plan] || PLAN_LIMITS[Plan.FREE]; + const limits = PLAN_LIMITS[plan] || PLAN_LIMITS[Plan.COMMUNITY]; // ───────────────────────────────────────────────────────────────────────── // METERED OPERATIONS - These consume quota diff --git a/apps/api/src/handlers/user.ts b/apps/api/src/handlers/user.ts index 372dca5..10320aa 100644 --- a/apps/api/src/handlers/user.ts +++ b/apps/api/src/handlers/user.ts @@ -132,7 +132,7 @@ export async function handleGetOwnLicense( } const plan = result.plan as PlanType; - const features = PLAN_FEATURES[plan] ?? PLAN_FEATURES.free; + const features = PLAN_FEATURES[plan] ?? PLAN_FEATURES.community; // Get usage count const licenseId = await getLicenseId(db, normalizedKey); @@ -217,7 +217,7 @@ export async function handleGetOwnMachines( } const plan = license.plan as PlanType; - const features = PLAN_FEATURES[plan] ?? PLAN_FEATURES.free; + const features = PLAN_FEATURES[plan] ?? PLAN_FEATURES.community; // Get machines const machinesResult = await db.all<{ diff --git a/apps/api/src/handlers/validate-license.ts b/apps/api/src/handlers/validate-license.ts index bb6e0ec..160b945 100644 --- a/apps/api/src/handlers/validate-license.ts +++ b/apps/api/src/handlers/validate-license.ts @@ -131,7 +131,7 @@ export async function handleValidateLicense( } const plan = license.plan as PlanType; - const features = PLAN_FEATURES[plan] ?? PLAN_FEATURES.free; + const features = PLAN_FEATURES[plan] ?? PLAN_FEATURES.community; // ───────────────────────────────────────────────────────────────────────── // Device Abuse Detection (check ACTIVE devices, not lifetime total) @@ -213,7 +213,7 @@ export async function handleValidateLicense( // Get new feature flags and limits const planEnum = license.plan as Plan; const featureFlags = getFeatureFlags(planEnum); - const newLimits = PLAN_LIMITS[planEnum] || PLAN_LIMITS[Plan.FREE]; + const newLimits = PLAN_LIMITS[planEnum] || PLAN_LIMITS[Plan.COMMUNITY]; // Build success response const response: ValidateLicenseResponse & { diff --git a/apps/api/src/lib/constants.ts b/apps/api/src/lib/constants.ts index 1381bb6..6bd4093 100644 --- a/apps/api/src/lib/constants.ts +++ b/apps/api/src/lib/constants.ts @@ -1,17 +1,50 @@ /** - * Constants and configuration for RepliMap Backend + * Constants and configuration for RepliMap Backend v4.0 + * + * Philosophy: "Gate Output, Not Input" + * - Unlimited scans for all tiers + * - Unlimited resources per scan + * - Charge when users export/download */ import type { PlanFeatures } from '../types'; import type { Env } from '../types/env'; // ============================================================================ -// Plan Configuration +// Plan Configuration v4.0 // ============================================================================ -export type PlanType = 'free' | 'solo' | 'pro' | 'team'; +export type PlanType = 'community' | 'pro' | 'team' | 'sovereign'; export type PlanBillingType = 'free' | 'monthly' | 'annual' | 'lifetime'; +/** Legacy plan names for backward compatibility */ +export type LegacyPlanType = 'free' | 'solo' | 'enterprise'; + +export const LEGACY_PLAN_MIGRATIONS: Record = { + free: 'community', + solo: 'pro', + enterprise: 'sovereign', +}; + +/** + * Normalize a plan name, converting legacy names to v4.0 names + */ +export function normalizePlanName(plan: string): PlanType { + const lower = plan.toLowerCase(); + + // Check v4.0 plan names + if (['community', 'pro', 'team', 'sovereign'].includes(lower)) { + return lower as PlanType; + } + + // Check legacy plan names + if (lower in LEGACY_PLAN_MIGRATIONS) { + return LEGACY_PLAN_MIGRATIONS[lower as LegacyPlanType]; + } + + return 'community'; // Default to community for unknown plans +} + // ============================================================================ // Lifetime Plan Constants // ============================================================================ @@ -20,33 +53,33 @@ export type PlanBillingType = 'free' | 'monthly' | 'annual' | 'lifetime'; export const LIFETIME_EXPIRY = '2099-12-31T23:59:59.000Z'; export const PLAN_FEATURES: Record = { - free: { - resources_per_scan: 5, - scans_per_month: 3, + community: { + resources_per_scan: -1, // v4.0: UNLIMITED + scans_per_month: -1, // v4.0: UNLIMITED aws_accounts: 1, machines: 1, - export_formats: ['terraform'], - }, - solo: { - resources_per_scan: -1, // unlimited - scans_per_month: -1, // unlimited - aws_accounts: 1, - machines: 2, - export_formats: ['terraform', 'cloudformation'], + export_formats: ['json'], // JSON only (with upgrade metadata) }, pro: { resources_per_scan: -1, scans_per_month: -1, aws_accounts: 3, - machines: 3, - export_formats: ['terraform', 'cloudformation'], + machines: 2, + export_formats: ['json', 'terraform', 'csv', 'html', 'markdown'], }, team: { resources_per_scan: -1, scans_per_month: -1, aws_accounts: 10, machines: 10, - export_formats: ['terraform', 'cloudformation'], + export_formats: ['json', 'terraform', 'csv', 'html', 'markdown', 'pdf'], + }, + sovereign: { + resources_per_scan: -1, + scans_per_month: -1, + aws_accounts: -1, // Unlimited + machines: -1, // Unlimited + export_formats: ['json', 'terraform', 'csv', 'html', 'markdown', 'pdf'], }, }; @@ -66,11 +99,15 @@ export const MAX_MACHINE_CHANGES_PER_MONTH = 3; * Used to detect upgrades vs downgrades */ export const PLAN_RANK: Record = { + // v4.0 plans + community: 0, + pro: 1, + team: 2, + sovereign: 3, + // Legacy plans (mapped to v4.0 equivalents) free: 0, solo: 1, - pro: 2, - team: 3, - enterprise: 4, + enterprise: 3, }; /** @@ -78,8 +115,8 @@ export const PLAN_RANK: Record = { * A downgrade means moving to a lower tier plan (fewer features/limits). */ export function isPlanDowngrade(oldPlan: string, newPlan: string): boolean { - const oldRank = PLAN_RANK[oldPlan] ?? 0; - const newRank = PLAN_RANK[newPlan] ?? 0; + const oldRank = PLAN_RANK[normalizePlanName(oldPlan)] ?? 0; + const newRank = PLAN_RANK[normalizePlanName(newPlan)] ?? 0; return newRank < oldRank; } @@ -87,8 +124,8 @@ export function isPlanDowngrade(oldPlan: string, newPlan: string): boolean { * Check if changing from oldPlan to newPlan is an upgrade. */ export function isPlanUpgrade(oldPlan: string, newPlan: string): boolean { - const oldRank = PLAN_RANK[oldPlan] ?? 0; - const newRank = PLAN_RANK[newPlan] ?? 0; + const oldRank = PLAN_RANK[normalizePlanName(oldPlan)] ?? 0; + const newRank = PLAN_RANK[normalizePlanName(newPlan)] ?? 0; return newRank > oldRank; } @@ -145,66 +182,87 @@ export const MACHINE_ID_PATTERN = /^[a-f0-9]{32}$/; export const AWS_ACCOUNT_ID_PATTERN = /^\d{12}$/; // ============================================================================ -// Plan Pricing -// Updated: 2025-12-25 -// New pricing: Solo $29, Pro $79, Team $149, Enterprise $399 +// Plan Pricing v4.0 +// Updated: 2025-01-15 +// v4.0 Pricing: Community $0, Pro $29, Team $99, Sovereign $2,500 // ============================================================================ /** * Plan prices in cents (monthly) */ export const PLAN_PRICES: Record = { + 'community': 0, + 'pro': 2900, // $29/month + 'team': 9900, // $99/month + 'sovereign': 250000, // $2,500/month + // Legacy plan prices (mapped to v4.0) 'free': 0, - 'solo': 2900, // $29/month - 'pro': 7900, // $79/month - 'team': 14900, // $149/month - 'enterprise': 39900, // $399/month + 'solo': 2900, // $29/month (same as PRO) + 'enterprise': 250000, // $2,500/month (same as SOVEREIGN) } as const; /** * Annual plan prices in cents (total per year) - * Significant discount compared to monthly billing + * 2 months free compared to monthly billing */ export const PLAN_ANNUAL_PRICES: Record = { + 'community': 0, + 'pro': 29000, // $290/year (~$24/month, 2 months free) + 'team': 99000, // $990/year (~$83/month, 2 months free) + 'sovereign': 2500000, // $25,000/year (~$2,083/month, 2 months free) + // Legacy plans 'free': 0, - 'solo': 19900, // $199/year (~$17/month, save $149) - 'pro': 59900, // $599/year (~$50/month, save $349) - 'team': 119900, // $1,199/year (~$100/month, save $589) - 'enterprise': 399900, // $3,999/year (~$333/month, save $789) + 'solo': 29000, + 'enterprise': 2500000, +} as const; + +/** + * Lifetime plan prices in cents (one-time payment) + */ +export const PLAN_LIFETIME_PRICES: Record = { + 'community': null, // No lifetime for free tier + 'pro': 19900, // $199 Early Bird (Regular: $249) + 'team': 49900, // $499 Early Bird (Regular: $699) + 'sovereign': null, // No lifetime for enterprise + // Legacy plans + 'free': null, + 'solo': 19900, + 'enterprise': null, } as const; // ============================================================================ -// Stripe Price ID to Plan Mapping +// Stripe Price ID to Plan Mapping v4.0 // ============================================================================ // TODO: Update these Stripe Price IDs after creating new prices in Stripe Dashboard -// Current IDs are for OLD pricing ($49/$99/$199) and need to be replaced export const STRIPE_PRICE_TO_PLAN: Record = { - // Development/test price IDs - Monthly (OLD PRICING - needs update) - 'price_1SiMWsAKLIiL9hdweoTnH17A': 'solo', // DONE: Replace with $29/mo price - 'price_1SiMYgAKLIiL9hdwZLjLUOPm': 'pro', // DONE: Replace with $79/mo price - 'price_1SiMZvAKLIiL9hdw8LAIvjrS': 'team', // DONE: Replace with $149/mo price - // Development/test price IDs - Annual (TODO: Create in Stripe) - 'price_1SiMpmAKLIiL9hdwhhn1dAVG': 'solo', // DONE: Create $199/year dev price - 'price_1SiMqMAKLIiL9hdwj1EgfQMs': 'pro', // DONE: Create $599/year dev price - 'price_1SiMrJAKLIiL9hdwF8xq4poz': 'team', // DONE: Create $1,199/year dev price + // Development/test price IDs - Monthly v4.0 + 'price_v4_pro_monthly': 'pro', + 'price_v4_team_monthly': 'team', + 'price_v4_sovereign_monthly': 'sovereign', + + // Development/test price IDs - Annual v4.0 + 'price_v4_pro_annual': 'pro', + 'price_v4_team_annual': 'team', + 'price_v4_sovereign_annual': 'sovereign', + + // Legacy price IDs (keep for backward compatibility) + 'price_1SiMWsAKLIiL9hdweoTnH17A': 'pro', // Legacy solo → pro + 'price_1SiMYgAKLIiL9hdwZLjLUOPm': 'pro', // Legacy pro → pro + 'price_1SiMZvAKLIiL9hdw8LAIvjrS': 'team', // Legacy team → team + 'price_1SiMpmAKLIiL9hdwhhn1dAVG': 'pro', // Legacy solo annual + 'price_1SiMqMAKLIiL9hdwj1EgfQMs': 'pro', // Legacy pro annual + 'price_1SiMrJAKLIiL9hdwF8xq4poz': 'team', // Legacy team annual + // Test price IDs (for unit testing) - Monthly - 'price_test_solo': 'solo', 'price_test_pro': 'pro', 'price_test_team': 'team', + 'price_test_sovereign': 'sovereign', + // Test price IDs (for unit testing) - Annual - 'price_test_solo_annual': 'solo', 'price_test_pro_annual': 'pro', 'price_test_team_annual': 'team', - // Production price IDs - add after creating products in Stripe: - // Monthly - // 'price_xxx_solo_monthly': 'solo', // $29/mo - // 'price_xxx_pro_monthly': 'pro', // $79/mo - // 'price_xxx_team_monthly': 'team', // $149/mo - // Annual - // 'price_xxx_solo_annual': 'solo', // $199/year - // 'price_xxx_pro_annual': 'pro', // $599/year - // 'price_xxx_team_annual': 'team', // $1,199/year + 'price_test_sovereign_annual': 'sovereign', }; /** @@ -212,31 +270,26 @@ export const STRIPE_PRICE_TO_PLAN: Record = { * TODO: Update these after creating new prices in Stripe Dashboard */ export const PLAN_TO_STRIPE_PRICE: Record = { - // Monthly prices (OLD PRICING - needs update) - 'solo': 'price_1SiMWsAKLIiL9hdweoTnH17A', // DONE: Create new $29/mo price - 'pro': 'price_1SiMYgAKLIiL9hdwZLjLUOPm', // DONE: Create new $79/mo price - 'team': 'price_1SiMZvAKLIiL9hdw8LAIvjrS', // DONE: Create new $149/mo price - // Production - uncomment and update after creating products in Stripe: - // 'solo': 'price_xxx_solo_monthly', - // 'pro': 'price_xxx_pro_monthly', - // 'team': 'price_xxx_team_monthly', + // v4.0 Monthly prices + 'pro': 'price_v4_pro_monthly', + 'team': 'price_v4_team_monthly', + 'sovereign': 'price_v4_sovereign_monthly', }; /** * Plan to Stripe Annual Price ID (for annual checkout sessions) - * TODO: Create annual price IDs in Stripe Dashboard */ export const PLAN_TO_STRIPE_ANNUAL_PRICE: Record = { - 'solo': 'price_1SiMpmAKLIiL9hdwhhn1dAVG', // DONE: Create $199/year price - 'pro': 'price_1SiMqMAKLIiL9hdwj1EgfQMs', // DONE: Create $599/year price - 'team': 'price_1SiMrJAKLIiL9hdwF8xq4poz', // DONE: Create $1,199/year price + 'pro': 'price_v4_pro_annual', + 'team': 'price_v4_team_annual', + 'sovereign': 'price_v4_sovereign_annual', }; /** * Get plan type from Stripe price ID */ export function getPlanFromPriceId(priceId: string): PlanType { - return STRIPE_PRICE_TO_PLAN[priceId] ?? 'free'; + return STRIPE_PRICE_TO_PLAN[priceId] ?? 'community'; } // ============================================================================ @@ -246,14 +299,15 @@ export function getPlanFromPriceId(priceId: string): PlanType { /** * Stripe Lifetime Price ID to Plan mapping. * These are one-time payment products, not subscriptions. - * - * Structure: { priceId: { plan, billingType } } */ export const STRIPE_LIFETIME_PRICE_TO_PLAN: Record = { - // Development/test lifetime price IDs - 'price_test_solo_lifetime': { plan: 'solo', billingType: 'lifetime' }, + // Development/test lifetime price IDs - v4.0 + 'price_v4_pro_lifetime': { plan: 'pro', billingType: 'lifetime' }, + 'price_v4_team_lifetime': { plan: 'team', billingType: 'lifetime' }, + + // Legacy test price IDs 'price_test_pro_lifetime': { plan: 'pro', billingType: 'lifetime' }, - // Production lifetime price IDs - will be added from env + 'price_test_team_lifetime': { plan: 'team', billingType: 'lifetime' }, }; /** @@ -276,11 +330,11 @@ export function getPlanInfoFromPriceId(priceId: string): { plan: PlanType; billi // Check annual prices const annualPrices = Object.values(PLAN_TO_STRIPE_ANNUAL_PRICE); if (annualPrices.includes(priceId)) { - return { plan: STRIPE_PRICE_TO_PLAN[priceId] ?? 'free', billingType: 'annual' }; + return { plan: STRIPE_PRICE_TO_PLAN[priceId] ?? 'community', billingType: 'annual' }; } // Default to monthly subscription - return { plan: STRIPE_PRICE_TO_PLAN[priceId] ?? 'free', billingType: 'monthly' }; + return { plan: STRIPE_PRICE_TO_PLAN[priceId] ?? 'community', billingType: 'monthly' }; } /** @@ -305,12 +359,12 @@ export function getStripePriceMapping(env: Env): Record = { - free: 3, - solo: 10, + community: 3, pro: 25, team: 50, - enterprise: -1, // Unlimited + sovereign: -1, // Unlimited + // Legacy plan names for backward compatibility + free: 3, + solo: 25, + enterprise: -1, }; // ============================================================================ diff --git a/apps/api/src/types/env.ts b/apps/api/src/types/env.ts index afa5277..43b72e2 100644 --- a/apps/api/src/types/env.ts +++ b/apps/api/src/types/env.ts @@ -15,8 +15,9 @@ export interface Env { // Stripe Lifetime Price IDs (optional - for one-time purchases) // These are one-time payment products, not subscriptions - STRIPE_SOLO_LIFETIME_PRICE_ID?: string; + STRIPE_SOLO_LIFETIME_PRICE_ID?: string; // Legacy - maps to pro STRIPE_PRO_LIFETIME_PRICE_ID?: string; + STRIPE_TEAM_LIFETIME_PRICE_ID?: string; // Machine signature verification (optional - for enhanced security) // If set, CLI must send HMAC-SHA256 signature of machine_id diff --git a/apps/web/src/components/pricing.tsx b/apps/web/src/components/pricing.tsx index a921948..57ef017 100644 --- a/apps/web/src/components/pricing.tsx +++ b/apps/web/src/components/pricing.tsx @@ -3,14 +3,14 @@ import { useState } from "react" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" -import { Check, X, Sparkles } from "lucide-react" -import { PLANS, ENTERPRISE_FEATURES, type PlanName, type BillingPeriod } from "@/lib/pricing" +import { Check, X, Sparkles, Shield } from "lucide-react" +import { PLANS, SOVEREIGN_FEATURES, type PlanName, type BillingPeriod } from "@/lib/pricing" const TALLY_FORM_URL = "https://tally.so/r/2EaYae" -// Transform PLANS object to array for rendering, excluding enterprise (shown separately) +// Transform PLANS object to array for rendering, excluding sovereign (shown separately) const plansList = (Object.entries(PLANS) as [PlanName, (typeof PLANS)[PlanName]][]) - .filter(([key]) => key !== "enterprise") + .filter(([key]) => key !== "sovereign") .map(([key, plan]) => ({ key, ...plan, @@ -38,7 +38,9 @@ export function Pricing() {

Simple, transparent pricing

-

Start free, upgrade when you need to export

+

+ Unlimited scanning. Pay only when you export. +

{/* Billing Toggle */} @@ -63,7 +65,7 @@ export function Pricing() { } rounded-md px-4 py-2 text-sm font-medium transition-colors flex items-center gap-1`} > Annual - (Save 17%) + (2 months free) )} @@ -177,21 +180,28 @@ export function Pricing() { ))} - {/* Enterprise Banner */} + {/* Sovereign Banner */}
- For Regulated Industries + + Sovereign Grade -

Enterprise

+

Sovereign

+

+ Data sovereignty for regulated industries +

- From $500 + From $2,500 /month
+

+ When your regulator asks “Where does the data go?”, the answer is: Nowhere. +

- {ENTERPRISE_FEATURES.map((feature, index) => ( + {SOVEREIGN_FEATURES.map((feature, index) => (
{feature} @@ -200,8 +210,8 @@ export function Pricing() {
diff --git a/apps/web/src/lib/pricing.ts b/apps/web/src/lib/pricing.ts index 97b1667..4a76c8b 100644 --- a/apps/web/src/lib/pricing.ts +++ b/apps/web/src/lib/pricing.ts @@ -1,15 +1,24 @@ /** - * RepliMap Pricing Configuration v3.2 + * RepliMap Pricing Configuration v4.0 * - * Gate Philosophy: "Gate Output, Not Input" - * - Scanning is unlimited (resources per scan) - * - Frequency is limited for free tier (3/month) - * - Output (download, export, details) is gated + * Philosophy: "Gate Output, Not Input" + * - Unlimited scans for all tiers + * - Unlimited resources per scan + * - Charge when users export/download + * + * Tier Structure: + * - COMMUNITY ($0): Full visibility, JSON export with metadata + * - PRO ($29): Terraform/CSV export, API access + * - TEAM ($99): Drift alerts, compliance reports, CI/CD + * - SOVEREIGN ($2,500): SSO, signed reports, air-gap, white-labeling */ -export type PlanName = "free" | "solo" | "pro" | "team" | "enterprise"; +export type PlanName = "community" | "pro" | "team" | "sovereign"; export type BillingPeriod = "monthly" | "annual" | "lifetime"; +/** Legacy plan names that map to v4.0 plans */ +export type LegacyPlanName = "free" | "solo" | "enterprise"; + export interface PlanPrice { monthly: number; annual: number; @@ -24,125 +33,175 @@ export interface PlanFeature { export interface Plan { name: string; + tagline: string; price: PlanPrice; description: string; features: PlanFeature[]; cta: string; highlighted: boolean; hasLifetime: boolean; + badge: string | null; } export const PLANS: Record = { - free: { - name: "Free", + community: { + name: "Community", + tagline: "Full visibility, export when ready", price: { monthly: 0, annual: 0, lifetime: null }, - description: "For evaluators exploring their infrastructure", + description: "See your entire AWS infrastructure. Upgrade when you're ready to take it home.", features: [ + { text: "Unlimited scans", included: true }, { text: "Unlimited resources per scan", included: true }, - { text: "3 full scans per month", included: true }, { text: "1 AWS account", included: true }, - { text: "Graph visualization", included: true }, - { text: "Code preview (100 lines)", included: true }, - { text: "Code download", included: false }, - { text: "Full remediation", included: false }, - { text: "Report export", included: false }, + { text: "Dependency graph visualization", included: true }, + { text: "Cost analysis (view)", included: true }, + { text: "Compliance issues (view)", included: true }, + { text: "Security score", included: true }, + { text: "JSON export (with upgrade prompts)", included: true }, + { text: "Terraform export", included: false }, + { text: "CSV export", included: false }, + { text: "API access", included: false }, ], - cta: "Get Started", + cta: "Get Started Free", highlighted: false, hasLifetime: false, - }, - solo: { - name: "Solo", - price: { monthly: 49, annual: 490, lifetime: 299 }, - description: "For individual DevOps professionals", - features: [ - { text: "Everything in Free", included: true }, - { text: "Unlimited scans", included: true }, - { text: "Full code download", included: true }, - { text: "Complete remediation steps", included: true }, - { text: "HTML report export", included: true }, - { text: "5 snapshots (7-day retention)", included: true }, - { text: "Email support (48h SLA)", included: true }, - { text: "Drift detection", included: false }, - ], - cta: "Start Solo", - highlighted: false, - hasLifetime: true, + badge: null, }, pro: { name: "Pro", - price: { monthly: 99, annual: 990, lifetime: 499 }, - description: "For senior engineers with multi-account needs", + tagline: "Export your infrastructure as code", + price: { monthly: 29, annual: 290, lifetime: 199 }, + description: "Take your Terraform code home. Perfect for individual DevOps engineers and SREs.", features: [ - { text: "Everything in Solo", included: true }, + { text: "Everything in Community", included: true }, { text: "3 AWS accounts", included: true }, - { text: "Drift detection", included: true }, - { text: "CI/CD integration", included: true }, - { text: "PDF report export", included: true }, - { text: "15 snapshots (30-day retention)", included: true }, - { text: "Remediate beta access", included: true, badge: "Priority" }, - { text: "Email support (24h SLA)", included: true }, + { text: "Terraform code export", included: true }, + { text: "CSV export", included: true }, + { text: "HTML/Markdown export", included: true }, + { text: "API access", included: true }, + { text: "Full audit findings", included: true }, + { text: "Audit remediation generator", included: true }, + { text: "Graph export (no watermark)", included: true }, + { text: "90-day history retention", included: true }, + { text: "48h email support SLA", included: true }, + { text: "Drift detection", included: false }, + { text: "Compliance reports", included: false }, ], - cta: "Start Pro", + cta: "Start Pro Trial", highlighted: true, hasLifetime: true, + badge: "Most Popular", }, team: { name: "Team", - price: { monthly: 199, annual: 1990, lifetime: null }, - description: "For teams with compliance needs", + tagline: "Continuous compliance for your organization", + price: { monthly: 99, annual: 990, lifetime: 499 }, + description: "Drift alerts, compliance reports, and CI/CD integration for growing teams.", features: [ { text: "Everything in Pro", included: true }, { text: "10 AWS accounts", included: true }, - { text: "Trust Center (audit logging)", included: true }, - { text: "API call recording", included: true }, - { text: "JSON report export", included: true }, - { text: "30 snapshots (90-day retention)", included: true }, - { text: "Email support (12h SLA)", included: true }, - { text: "Compliance mapping", included: false }, + { text: "5 team members", included: true }, + { text: "Drift detection", included: true }, + { text: "Drift alerts (Slack/Teams/Webhook)", included: true }, + { text: "CI/CD integration (--fail-on)", included: true }, + { text: "CIS Benchmark reports", included: true }, + { text: "SOC2 compliance mapping", included: true }, + { text: "PDF export", included: true }, + { text: "Custom compliance rules", included: true }, + { text: "365-day history retention", included: true }, + { text: "12h priority support SLA", included: true }, + { text: "SSO (SAML/OIDC)", included: false }, ], - cta: "Start Team", + cta: "Start Team Trial", highlighted: false, - hasLifetime: false, + hasLifetime: true, + badge: null, }, - enterprise: { - name: "Enterprise", - price: { monthly: 500, annual: 5000, lifetime: null }, - description: "For banks and regulated industries", + sovereign: { + name: "Sovereign", + tagline: "Data sovereignty for regulated industries", + price: { monthly: 2500, annual: 25000, lifetime: null }, + description: "When your regulator asks 'Where does the data go?', the answer is: Nowhere.", features: [ { text: "Everything in Team", included: true }, { text: "Unlimited AWS accounts", included: true }, + { text: "Unlimited team members", included: true }, + { text: "SSO (SAML/OIDC)", included: true }, { text: "APRA CPS 234 mapping", included: true }, - { text: "RBNZ BS11 mapping", included: true }, + { text: "DORA compliance", included: true }, { text: "Essential Eight assessment", included: true }, - { text: "Digital signatures (SHA256)", included: true }, - { text: "Tamper-evident reports", included: true }, + { text: "Custom compliance mapping", included: true }, + { text: "SHA256 signed reports", included: true }, + { text: "Tamper-evident audit trail", included: true }, + { text: "Air-gap deployment", included: true }, + { text: "White-labeling", included: true }, { text: "4h SLA support", included: true }, + { text: "Dedicated account manager", included: true }, ], - cta: "Contact Sales", + cta: "Request Demo", highlighted: false, hasLifetime: false, + badge: "Sovereign Grade", }, }; -export const ENTERPRISE_FEATURES = [ +export const SOVEREIGN_FEATURES = [ "Unlimited AWS accounts", + "Unlimited team members", + "SSO (SAML/OIDC)", "APRA CPS 234 mapping", - "RBNZ BS11 mapping", + "DORA compliance", "Essential Eight assessment", - "Digital signatures (SHA256)", - "Tamper-evident reports", - "Unlimited snapshots (1-year retention)", + "RBNZ BS11 mapping", + "Custom compliance frameworks", + "SHA256 signed reports", + "Tamper-evident audit trail", + "Air-gap deployment", + "White-labeling", "4h SLA support", + "Dedicated account manager", ]; +// ============================================================================= +// Legacy Plan Migration +// ============================================================================= + +export const LEGACY_PLAN_MIGRATIONS: Record = { + free: "community", + solo: "pro", + enterprise: "sovereign", +}; + +/** + * Normalize a plan name, converting legacy names to v4.0 names + */ +export function normalizePlanName(plan: string): PlanName { + const lower = plan.toLowerCase(); + if (lower in PLANS) return lower as PlanName; + if (lower in LEGACY_PLAN_MIGRATIONS) { + return LEGACY_PLAN_MIGRATIONS[lower as LegacyPlanName]; + } + return "community"; // Default to community for unknown plans +} + +/** + * Check if a plan name is a legacy plan + */ +export function isLegacyPlan(plan: string): boolean { + return plan.toLowerCase() in LEGACY_PLAN_MIGRATIONS; +} + +// ============================================================================= +// Helper Functions +// ============================================================================= + export function formatPrice( amount: number, period: BillingPeriod ): string { if (amount === 0) return "Free"; - const formattedAmount = `$${amount}`; + const formattedAmount = `$${amount.toLocaleString()}`; switch (period) { case "monthly": @@ -179,3 +238,41 @@ export function getLifetimeBreakeven(plan: Plan): number { if (!plan.price.lifetime || !plan.price.monthly) return 0; return Math.ceil(plan.price.lifetime / plan.price.monthly); } + +// ============================================================================= +// Plan Comparison +// ============================================================================= + +export const PLAN_RANK: Record = { + // v4.0 plans + community: 0, + pro: 1, + team: 2, + sovereign: 3, + // Legacy plans + free: 0, + solo: 1, + enterprise: 3, +}; + +export function isPlanUpgrade(from: string, to: string): boolean { + const fromRank = PLAN_RANK[normalizePlanName(from)] ?? 0; + const toRank = PLAN_RANK[normalizePlanName(to)] ?? 0; + return toRank > fromRank; +} + +export function isPlanDowngrade(from: string, to: string): boolean { + const fromRank = PLAN_RANK[normalizePlanName(from)] ?? 0; + const toRank = PLAN_RANK[normalizePlanName(to)] ?? 0; + return toRank < fromRank; +} + +export function getUpgradePath(currentPlan: PlanName): PlanName | null { + const upgradePaths: Record = { + community: "pro", + pro: "team", + team: "sovereign", + sovereign: null, + }; + return upgradePaths[currentPlan]; +} diff --git a/packages/config/dist/config.py b/packages/config/dist/config.py index 65a5600..b919d51 100644 --- a/packages/config/dist/config.py +++ b/packages/config/dist/config.py @@ -1,8 +1,12 @@ """ -@replimap/config - Auto-generated configuration +@replimap/config v4.0 - Auto-generated configuration DO NOT EDIT - This file is generated from src/*.json -Content Hash: 7abec13bd128 +Philosophy: "Gate Output, Not Input" +- Unlimited scans for all tiers +- Charge when users export/download + +Content Hash: fd3749d4c50e """ from __future__ import annotations @@ -10,67 +14,169 @@ from typing import Literal, Optional -# ============================================================================ +# ============================================================================= # Version -# ============================================================================ +# ============================================================================= + +CONFIG_VERSION: str = "fd3749d4c50e" -CONFIG_VERSION: str = "7abec13bd128" +# ============================================================================= +# Plan Types +# ============================================================================= -# ============================================================================ -# Plans -# ============================================================================ +PlanName = Literal["community", "pro", "team", "sovereign"] +LegacyPlanName = Literal["free", "solo", "enterprise"] -PlanName = Literal["free", "pro", "team", "sovereign"] +PLAN_NAMES: tuple[PlanName, ...] = ("community", "pro", "team", "sovereign",) -PLAN_NAMES: tuple[PlanName, ...] = ("free", "pro", "team", "sovereign",) +LEGACY_PLAN_MIGRATIONS: dict[LegacyPlanName, PlanName] = { + "free": "community", + "solo": "pro", + "enterprise": "sovereign", +} + + +@dataclass(frozen=True) +class PlanUI: + """UI configuration for a plan.""" + cta: str + badge: Optional[str] + highlight: bool + + +@dataclass(frozen=True) +class PlanAddOn: + """Configuration for a plan add-on.""" + name: str + description: str + price_monthly: int # cents @dataclass(frozen=True) class PlanConfig: """Configuration for a pricing plan.""" - price_monthly: int - scans_per_month: Optional[int] - max_accounts: Optional[int] + name: str + tagline: str + description: str + price_monthly: int # cents + price_yearly: int # cents + price_lifetime: Optional[int] # cents, None if not available + scans_per_month: Optional[int] # None = unlimited + resources_per_scan: Optional[int] # None = unlimited + aws_accounts: Optional[int] # None = unlimited + team_members: Optional[int] # None = unlimited + machines: Optional[int] # None = unlimited + history_retention_days: Optional[int] # None = unlimited features: list[str] - addons: Optional[dict[str, int]] = None + ui: PlanUI + addons: Optional[dict[str, PlanAddOn]] = None PLANS: dict[PlanName, PlanConfig] = { - "free": PlanConfig( + "community": PlanConfig( + name="COMMUNITY", + tagline="Full visibility, export when ready", + description="See your entire AWS infrastructure. Upgrade when you\'re ready to take it home.", price_monthly=0, - scans_per_month=10, - max_accounts=None, - features=["basic_scan","graph_preview"], + price_yearly=0, + price_lifetime=None, + scans_per_month=None, + resources_per_scan=None, + aws_accounts=1, + team_members=1, + machines=1, + history_retention_days=7, + features=["basic_scanning","dependency_graph","cost_analysis","compliance_view","security_score","export_json","snapshot","snapshot_diff"], + ui=PlanUI(cta="Get Started Free", badge=None, highlight=False), addons=None, ), "pro": PlanConfig( + name="PRO", + tagline="Export your infrastructure as code", + description="Take your Terraform code home. Perfect for individual DevOps engineers and SREs.", price_monthly=2900, + price_yearly=29000, + price_lifetime=19900, scans_per_month=None, - max_accounts=None, - features=["basic_scan","graph_preview","terraform_download","full_audit"], + resources_per_scan=None, + aws_accounts=3, + team_members=1, + machines=2, + history_retention_days=90, + features=["basic_scanning","dependency_graph","cost_analysis","compliance_view","security_score","export_json","export_terraform","export_csv","export_html","export_markdown","graph_full","graph_security","graph_export_no_watermark","audit_full_findings","audit_fix","audit_report_export","snapshot","snapshot_diff","api_access"], + ui=PlanUI(cta="Start PRO Trial", badge="Most Popular", highlight=True), addons=None, ), "team": PlanConfig( + name="TEAM", + tagline="Continuous compliance for your organization", + description="Drift alerts, compliance reports, and CI/CD integration for growing teams.", price_monthly=9900, + price_yearly=99000, + price_lifetime=49900, scans_per_month=None, - max_accounts=10, - features=["*"], + resources_per_scan=None, + aws_accounts=10, + team_members=5, + machines=10, + history_retention_days=365, + features=["*","!compliance_reports_apra","!compliance_reports_dora","!compliance_reports_essential8","!custom_compliance_mapping","!sso","!dedicated_support","!air_gap_deployment","!report_signature","!tamper_evident_audit_trail","!white_labeling"], + ui=PlanUI(cta="Start TEAM Trial", badge=None, highlight=False), addons=None, ), "sovereign": PlanConfig( + name="SOVEREIGN", + tagline="Data sovereignty for regulated industries", + description="When your regulator asks \'Where does the data go?\', the answer is: Nowhere.", price_monthly=250000, + price_yearly=2500000, + price_lifetime=None, scans_per_month=None, - max_accounts=None, + resources_per_scan=None, + aws_accounts=None, + team_members=None, + machines=None, + history_retention_days=None, features=["*"], - addons={"apra_cps234": 50000, "essential_eight": 30000, "rbnz_bs11": 40000, "dora": 50000}, + ui=PlanUI(cta="Request Demo", badge="Sovereign Grade", highlight=False), + addons={"apra_cps234": PlanAddOn(name="APRA CPS 234 Module", description="Australian Prudential Regulation Authority compliance mapping", price_monthly=50000), "dora": PlanAddOn(name="DORA Compliance Module", description="Digital Operational Resilience Act (EU) compliance mapping", price_monthly=50000), "essential_eight": PlanAddOn(name="Essential Eight Module", description="Australian Cyber Security Centre maturity assessment", price_monthly=30000), "rbnz_bs11": PlanAddOn(name="RBNZ BS11 Module", description="Reserve Bank of New Zealand outsourcing requirements", price_monthly=30000), "dedicated_am": PlanAddOn(name="Dedicated Account Manager", description="Named contact for your organization", price_monthly=50000), "sla_999": PlanAddOn(name="99.9% SLA Guarantee", description="Uptime guarantee with financial penalties", price_monthly=20000)}, ), } -# ============================================================================ +# ============================================================================= +# Features +# ============================================================================= + +ALL_FEATURES: tuple[str, ...] = ( + "api_access", + "audit_fix", + "audit_full_findings", + "audit_report_export", + "basic_scanning", + "compliance_view", + "cost_analysis", + "dependency_graph", + "export_csv", + "export_html", + "export_json", + "export_markdown", + "export_terraform", + "graph_export_no_watermark", + "graph_full", + "graph_security", + "security_score", + "snapshot", + "snapshot_diff", +) + +FeatureName = Literal["api_access", "audit_fix", "audit_full_findings", "audit_report_export", "basic_scanning", "compliance_view", "cost_analysis", "dependency_graph", "export_csv", "export_html", "export_json", "export_markdown", "export_terraform", "graph_export_no_watermark", "graph_full", "graph_security", "security_score", "snapshot", "snapshot_diff"] + + +# ============================================================================= # Compliance Frameworks -# ============================================================================ +# ============================================================================= FrameworkId = Literal["apra_cps234", "essential_eight", "rbnz_bs11", "dora", "soc2", "iso27001"] @@ -126,9 +232,9 @@ class FrameworkConfig: } -# ============================================================================ +# ============================================================================= # AWS Resources -# ============================================================================ +# ============================================================================= ResourceCategory = Literal["compute", "storage", "database", "networking", "security", "messaging", "monitoring"] @@ -191,15 +297,29 @@ class FrameworkConfig: AwsResourceType = Literal["aws_instance", "aws_lambda_function", "aws_ecs_cluster", "aws_ecs_service", "aws_ecs_task_definition", "aws_eks_cluster", "aws_autoscaling_group", "aws_s3_bucket", "aws_ebs_volume", "aws_efs_file_system", "aws_fsx_lustre_file_system", "aws_db_instance", "aws_rds_cluster", "aws_dynamodb_table", "aws_elasticache_cluster", "aws_redshift_cluster", "aws_vpc", "aws_subnet", "aws_security_group", "aws_network_acl", "aws_route_table", "aws_internet_gateway", "aws_nat_gateway", "aws_lb", "aws_lb_target_group", "aws_cloudfront_distribution", "aws_route53_zone", "aws_iam_role", "aws_iam_policy", "aws_iam_user", "aws_iam_group", "aws_kms_key", "aws_secretsmanager_secret", "aws_acm_certificate", "aws_sqs_queue", "aws_sns_topic", "aws_kinesis_stream", "aws_eventbridge_rule", "aws_cloudwatch_log_group", "aws_cloudwatch_metric_alarm", "aws_cloudtrail"] -# ============================================================================ +# ============================================================================= # Helper Functions -# ============================================================================ +# ============================================================================= def is_plan_name(value: str) -> bool: """Check if a string is a valid plan name.""" return value in PLAN_NAMES +def is_legacy_plan_name(value: str) -> bool: + """Check if a string is a legacy plan name.""" + return value in LEGACY_PLAN_MIGRATIONS + + +def normalize_plan_name(value: str) -> PlanName: + """Normalize a plan name, converting legacy names to v4.0 names.""" + if is_plan_name(value): + return value # type: ignore + if is_legacy_plan_name(value): + return LEGACY_PLAN_MIGRATIONS[value] # type: ignore + return "community" + + def is_framework_id(value: str) -> bool: """Check if a string is a valid framework ID.""" return value in FRAMEWORK_IDS @@ -210,9 +330,94 @@ def is_aws_resource_type(value: str) -> bool: return value in ALL_AWS_RESOURCES +def plan_has_feature(plan: PlanName, feature: str) -> bool: + """Check if a plan has a specific feature.""" + config = PLANS[plan] + + # Check for "*" (all features) + if "*" in config.features: + # Check if explicitly excluded + return f"!{feature}" not in config.features + + return feature in config.features + + def get_plan_features(plan: PlanName) -> list[str]: - """Get the list of features for a plan.""" + """Get all features for a plan (resolving * and ! modifiers).""" config = PLANS[plan] + if "*" in config.features: - return ["basic_scan", "graph_preview", "terraform_download", "full_audit"] - return list(config.features) + # All features except excluded ones + excluded = [f[1:] for f in config.features if f.startswith("!")] + return [f for f in ALL_FEATURES if f not in excluded] + + return [f for f in config.features if not f.startswith("!")] + + +def is_unlimited(value: Optional[int]) -> bool: + """Check if a limit is unlimited (None or -1).""" + return value is None or value == -1 + + +def format_limit(value: Optional[int]) -> str: + """Format a limit for display.""" + if is_unlimited(value): + return "Unlimited" + return f"{value:,}" + + +def format_price(cents: int) -> str: + """Format price in dollars from cents.""" + if cents == 0: + return "Free" + return f"${cents / 100:,.0f}" + + +def get_required_plan_for_feature(feature: str) -> PlanName: + """Get the minimum plan required for a feature.""" + for plan_name in PLAN_NAMES: + if plan_has_feature(plan_name, feature): + return plan_name + return "sovereign" + + +def get_upgrade_path(current_plan: PlanName) -> Optional[PlanName]: + """Get upgrade path from current plan.""" + upgrade_paths: dict[PlanName, Optional[PlanName]] = { + "community": "pro", + "pro": "team", + "team": "sovereign", + "sovereign": None, + } + return upgrade_paths.get(current_plan) + + +# ============================================================================= +# Plan Comparison +# ============================================================================= + +PLAN_RANK: dict[str, int] = { + # v4.0 plans + "community": 0, + "pro": 1, + "team": 2, + "sovereign": 3, + # Legacy plans + "free": 0, + "solo": 1, + "enterprise": 3, +} + + +def is_plan_upgrade(from_plan: str, to_plan: str) -> bool: + """Check if changing plans is an upgrade.""" + from_rank = PLAN_RANK.get(normalize_plan_name(from_plan), 0) + to_rank = PLAN_RANK.get(normalize_plan_name(to_plan), 0) + return to_rank > from_rank + + +def is_plan_downgrade(from_plan: str, to_plan: str) -> bool: + """Check if changing plans is a downgrade.""" + from_rank = PLAN_RANK.get(normalize_plan_name(from_plan), 0) + to_rank = PLAN_RANK.get(normalize_plan_name(to_plan), 0) + return to_rank < from_rank diff --git a/packages/config/dist/index.ts b/packages/config/dist/index.ts index afd00f3..1accf81 100644 --- a/packages/config/dist/index.ts +++ b/packages/config/dist/index.ts @@ -1,77 +1,266 @@ /** - * @replimap/config - Auto-generated configuration + * @replimap/config v4.0 - Auto-generated configuration * DO NOT EDIT - This file is generated from src/*.json * - * Content Hash: 7abec13bd128 + * Philosophy: "Gate Output, Not Input" + * - Unlimited scans for all tiers + * - Charge when users export/download + * + * Content Hash: fd3749d4c50e */ -// ============================================================================ +// ============================================================================= // Version -// ============================================================================ +// ============================================================================= -export const CONFIG_VERSION = "7abec13bd128" as const; +export const CONFIG_VERSION = "fd3749d4c50e" as const; -// ============================================================================ -// Plans -// ============================================================================ +// ============================================================================= +// Plan Types +// ============================================================================= -export const PLAN_NAMES = ["free", "pro", "team", "sovereign"] as const; +export const PLAN_NAMES = ["community", "pro", "team", "sovereign"] as const; export type PlanName = typeof PLAN_NAMES[number]; +/** Legacy plan names that map to v4.0 plans */ +export const LEGACY_PLAN_MIGRATIONS: Record = { + free: "community", + solo: "pro", + enterprise: "sovereign", +} as const; + +export type LegacyPlanName = "free" | "solo" | "enterprise"; + +export interface PlanUI { + cta: string; + badge: string | null; + highlight: boolean; +} + +export interface PlanAddOn { + name: string; + description: string; + /** Price in cents per month */ + price_monthly: number; +} + export interface PlanConfig { + name: string; + tagline: string; + description: string; + /** Price in cents per month */ price_monthly: number; + /** Price in cents per year */ + price_yearly: number; + /** Price in cents for lifetime (null if not available) */ + price_lifetime: number | null; + /** Scans per month (null = unlimited) */ scans_per_month: number | null; - max_accounts?: number | null; + /** Resources per scan (null = unlimited) */ + resources_per_scan: number | null; + /** Maximum AWS accounts (null = unlimited) */ + aws_accounts: number | null; + /** Maximum team members (null = unlimited) */ + team_members: number | null; + /** Maximum machines (null = unlimited) */ + machines: number | null; + /** History retention in days (null = unlimited) */ + history_retention_days: number | null; + /** Feature flags - "*" means all features, "!feature" means excluded */ features: string[]; - addons?: Record; + ui: PlanUI; + addons?: Record; } export const PLANS: Record = { - "free": { + "community": { + "name": "COMMUNITY", + "tagline": "Full visibility, export when ready", + "description": "See your entire AWS infrastructure. Upgrade when you're ready to take it home.", "price_monthly": 0, - "scans_per_month": 10, + "price_yearly": 0, + "price_lifetime": null, + "scans_per_month": null, + "resources_per_scan": null, + "aws_accounts": 1, + "team_members": 1, + "machines": 1, + "history_retention_days": 7, "features": [ - "basic_scan", - "graph_preview" - ] + "basic_scanning", + "dependency_graph", + "cost_analysis", + "compliance_view", + "security_score", + "export_json", + "snapshot", + "snapshot_diff" + ], + "ui": { + "cta": "Get Started Free", + "badge": null, + "highlight": false + } }, "pro": { + "name": "PRO", + "tagline": "Export your infrastructure as code", + "description": "Take your Terraform code home. Perfect for individual DevOps engineers and SREs.", "price_monthly": 2900, + "price_yearly": 29000, + "price_lifetime": 19900, "scans_per_month": null, + "resources_per_scan": null, + "aws_accounts": 3, + "team_members": 1, + "machines": 2, + "history_retention_days": 90, "features": [ - "basic_scan", - "graph_preview", - "terraform_download", - "full_audit" - ] + "basic_scanning", + "dependency_graph", + "cost_analysis", + "compliance_view", + "security_score", + "export_json", + "export_terraform", + "export_csv", + "export_html", + "export_markdown", + "graph_full", + "graph_security", + "graph_export_no_watermark", + "audit_full_findings", + "audit_fix", + "audit_report_export", + "snapshot", + "snapshot_diff", + "api_access" + ], + "ui": { + "cta": "Start PRO Trial", + "badge": "Most Popular", + "highlight": true + } }, "team": { + "name": "TEAM", + "tagline": "Continuous compliance for your organization", + "description": "Drift alerts, compliance reports, and CI/CD integration for growing teams.", "price_monthly": 9900, + "price_yearly": 99000, + "price_lifetime": 49900, "scans_per_month": null, - "max_accounts": 10, + "resources_per_scan": null, + "aws_accounts": 10, + "team_members": 5, + "machines": 10, + "history_retention_days": 365, "features": [ - "*" - ] + "*", + "!compliance_reports_apra", + "!compliance_reports_dora", + "!compliance_reports_essential8", + "!custom_compliance_mapping", + "!sso", + "!dedicated_support", + "!air_gap_deployment", + "!report_signature", + "!tamper_evident_audit_trail", + "!white_labeling" + ], + "ui": { + "cta": "Start TEAM Trial", + "badge": null, + "highlight": false + } }, "sovereign": { + "name": "SOVEREIGN", + "tagline": "Data sovereignty for regulated industries", + "description": "When your regulator asks 'Where does the data go?', the answer is: Nowhere.", "price_monthly": 250000, + "price_yearly": 2500000, + "price_lifetime": null, "scans_per_month": null, - "max_accounts": null, + "resources_per_scan": null, + "aws_accounts": null, + "team_members": null, + "machines": null, + "history_retention_days": null, "features": [ "*" ], "addons": { - "apra_cps234": 50000, - "essential_eight": 30000, - "rbnz_bs11": 40000, - "dora": 50000 + "apra_cps234": { + "name": "APRA CPS 234 Module", + "description": "Australian Prudential Regulation Authority compliance mapping", + "price_monthly": 50000 + }, + "dora": { + "name": "DORA Compliance Module", + "description": "Digital Operational Resilience Act (EU) compliance mapping", + "price_monthly": 50000 + }, + "essential_eight": { + "name": "Essential Eight Module", + "description": "Australian Cyber Security Centre maturity assessment", + "price_monthly": 30000 + }, + "rbnz_bs11": { + "name": "RBNZ BS11 Module", + "description": "Reserve Bank of New Zealand outsourcing requirements", + "price_monthly": 30000 + }, + "dedicated_am": { + "name": "Dedicated Account Manager", + "description": "Named contact for your organization", + "price_monthly": 50000 + }, + "sla_999": { + "name": "99.9% SLA Guarantee", + "description": "Uptime guarantee with financial penalties", + "price_monthly": 20000 + } + }, + "ui": { + "cta": "Request Demo", + "badge": "Sovereign Grade", + "highlight": false } } } as const; -// ============================================================================ +// ============================================================================= +// Features +// ============================================================================= + +export const ALL_FEATURES = [ + "api_access", + "audit_fix", + "audit_full_findings", + "audit_report_export", + "basic_scanning", + "compliance_view", + "cost_analysis", + "dependency_graph", + "export_csv", + "export_html", + "export_json", + "export_markdown", + "export_terraform", + "graph_export_no_watermark", + "graph_full", + "graph_security", + "security_score", + "snapshot", + "snapshot_diff" +] as const; + +export type FeatureName = typeof ALL_FEATURES[number]; + +// ============================================================================= // Compliance Frameworks -// ============================================================================ +// ============================================================================= export const FRAMEWORK_IDS = ["apra_cps234", "essential_eight", "rbnz_bs11", "dora", "soc2", "iso27001"] as const; export type FrameworkId = typeof FRAMEWORK_IDS[number]; @@ -122,9 +311,9 @@ export const COMPLIANCE_FRAMEWORKS: Record = { } } as const; -// ============================================================================ +// ============================================================================= // AWS Resources -// ============================================================================ +// ============================================================================= export const RESOURCE_CATEGORIES = ["compute", "storage", "database", "networking", "security", "messaging", "monitoring"] as const; export type ResourceCategory = typeof RESOURCE_CATEGORIES[number]; @@ -233,14 +422,27 @@ export const ALL_AWS_RESOURCES = [ export type AwsResourceType = typeof ALL_AWS_RESOURCES[number]; -// ============================================================================ +// ============================================================================= // Helper Functions -// ============================================================================ +// ============================================================================= export function isPlanName(value: string): value is PlanName { return PLAN_NAMES.includes(value as PlanName); } +export function isLegacyPlanName(value: string): value is LegacyPlanName { + return value in LEGACY_PLAN_MIGRATIONS; +} + +/** + * Normalize a plan name, converting legacy names to v4.0 names + */ +export function normalizePlanName(value: string): PlanName { + if (isPlanName(value)) return value; + if (isLegacyPlanName(value)) return LEGACY_PLAN_MIGRATIONS[value]; + return "community"; // Default to community for unknown plans +} + export function isFrameworkId(value: string): value is FrameworkId { return FRAMEWORK_IDS.includes(value as FrameworkId); } @@ -249,11 +451,110 @@ export function isAwsResourceType(value: string): value is AwsResourceType { return ALL_AWS_RESOURCES.includes(value as AwsResourceType); } +/** + * Check if a plan has a specific feature + */ +export function planHasFeature(plan: PlanName, feature: string): boolean { + const config = PLANS[plan]; + + // Check for "*" (all features) + if (config.features.includes("*")) { + // Check if explicitly excluded + return !config.features.includes(`!${feature}`); + } + + return config.features.includes(feature); +} + +/** + * Get all features for a plan (resolving "*" and "!" modifiers) + */ export function getPlanFeatures(plan: PlanName): string[] { const config = PLANS[plan]; + if (config.features.includes("*")) { - // Return all possible features - return ["basic_scan", "graph_preview", "terraform_download", "full_audit"]; + // All features except excluded ones + const excluded = config.features + .filter(f => f.startsWith("!")) + .map(f => f.slice(1)); + return ALL_FEATURES.filter(f => !excluded.includes(f)); + } + + return config.features.filter(f => !f.startsWith("!")); +} + +/** + * Check if a limit is unlimited (null or -1) + */ +export function isUnlimited(value: number | null): boolean { + return value === null || value === -1; +} + +/** + * Format a limit for display + */ +export function formatLimit(value: number | null): string { + if (isUnlimited(value)) return "Unlimited"; + return value!.toLocaleString(); +} + +/** + * Format price in dollars from cents + */ +export function formatPrice(cents: number): string { + if (cents === 0) return "Free"; + return `$${(cents / 100).toLocaleString()}`; +} + +/** + * Get the minimum plan required for a feature + */ +export function getRequiredPlanForFeature(feature: string): PlanName { + for (const planName of PLAN_NAMES) { + if (planHasFeature(planName, feature)) { + return planName; + } } - return config.features; + return "sovereign"; // Default to highest tier +} + +/** + * Get upgrade path from current plan + */ +export function getUpgradePath(currentPlan: PlanName): PlanName | null { + const upgradePaths: Record = { + community: "pro", + pro: "team", + team: "sovereign", + sovereign: null, + }; + return upgradePaths[currentPlan]; +} + +// ============================================================================= +// Plan Comparison +// ============================================================================= + +export const PLAN_RANK: Record = { + // v4.0 plans + community: 0, + pro: 1, + team: 2, + sovereign: 3, + // Legacy plans (mapped to their v4.0 equivalents) + free: 0, + solo: 1, + enterprise: 3, +}; + +export function isPlanUpgrade(from: string, to: string): boolean { + const fromRank = PLAN_RANK[normalizePlanName(from)] ?? 0; + const toRank = PLAN_RANK[normalizePlanName(to)] ?? 0; + return toRank > fromRank; +} + +export function isPlanDowngrade(from: string, to: string): boolean { + const fromRank = PLAN_RANK[normalizePlanName(from)] ?? 0; + const toRank = PLAN_RANK[normalizePlanName(to)] ?? 0; + return toRank < fromRank; } diff --git a/packages/config/scripts/build.ts b/packages/config/scripts/build.ts index ddca182..035e966 100644 --- a/packages/config/scripts/build.ts +++ b/packages/config/scripts/build.ts @@ -1,17 +1,19 @@ #!/usr/bin/env tsx /** - * Code generation script for @replimap/config + * Code generation script for @replimap/config v4.0 * * Reads JSON source files and generates: * - dist/index.ts (TypeScript exports) * - dist/config.py (Python dataclasses) * + * Philosophy: "Gate Output, Not Input" + * * Usage: * pnpm build # Generate files * pnpm check # Verify generated files are up to date */ -import { readFileSync, writeFileSync, readdirSync, existsSync } from "node:fs"; +import { readFileSync, writeFileSync, existsSync } from "node:fs"; import { createHash } from "node:crypto"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; @@ -22,14 +24,42 @@ const ROOT = join(__dirname, ".."); const SRC_DIR = join(ROOT, "src"); const DIST_DIR = join(ROOT, "dist"); -interface Plans { - [key: string]: { - price_monthly: number; - scans_per_month: number | null; - max_accounts?: number | null; - features: string[]; - addons?: Record; - }; +// ============================================================================= +// Type Definitions for v4.0 Plans +// ============================================================================= + +interface PlanUI { + cta: string; + badge: string | null; + highlight: boolean; +} + +interface PlanAddOn { + name: string; + description: string; + price_monthly: number; +} + +interface PlanConfigV4 { + name: string; + tagline: string; + description: string; + price_monthly: number; + price_yearly: number; + price_lifetime: number | null; + scans_per_month: number | null; + resources_per_scan: number | null; + aws_accounts: number | null; + team_members: number | null; + machines: number | null; + history_retention_days: number | null; + features: string[]; + ui: PlanUI; + addons?: Record; +} + +interface PlansV4 { + [key: string]: PlanConfigV4; } interface Framework { @@ -52,7 +82,7 @@ function computeContentHash(content: string): string { } function loadJsonFiles(): { - plans: Plans; + plans: PlansV4; frameworks: Frameworks; resources: Resources; combinedHash: string; @@ -67,7 +97,7 @@ function loadJsonFiles(): { "utf-8" ); - const plans = JSON.parse(plansContent) as Plans; + const plans = JSON.parse(plansContent) as PlansV4; const frameworks = JSON.parse(frameworksContent) as Frameworks; const resources = JSON.parse(resourcesContent) as Resources; @@ -79,7 +109,7 @@ function loadJsonFiles(): { } function generateTypeScript( - plans: Plans, + plans: PlansV4, frameworks: Frameworks, resources: Resources, hash: string @@ -88,39 +118,108 @@ function generateTypeScript( const frameworkIds = Object.keys(frameworks.compliance_frameworks); const resourceCategories = Object.keys(resources.aws_resources); + // Collect all unique features from all plans + const allFeatures = new Set(); + for (const plan of Object.values(plans)) { + for (const feature of plan.features) { + if (feature !== "*" && !feature.startsWith("!")) { + allFeatures.add(feature); + } + } + } + return `/** - * @replimap/config - Auto-generated configuration + * @replimap/config v4.0 - Auto-generated configuration * DO NOT EDIT - This file is generated from src/*.json * + * Philosophy: "Gate Output, Not Input" + * - Unlimited scans for all tiers + * - Charge when users export/download + * * Content Hash: ${hash} */ -// ============================================================================ +// ============================================================================= // Version -// ============================================================================ +// ============================================================================= export const CONFIG_VERSION = "${hash}" as const; -// ============================================================================ -// Plans -// ============================================================================ +// ============================================================================= +// Plan Types +// ============================================================================= export const PLAN_NAMES = [${planNames.map((n) => `"${n}"`).join(", ")}] as const; export type PlanName = typeof PLAN_NAMES[number]; +/** Legacy plan names that map to v4.0 plans */ +export const LEGACY_PLAN_MIGRATIONS: Record = { + free: "community", + solo: "pro", + enterprise: "sovereign", +} as const; + +export type LegacyPlanName = "free" | "solo" | "enterprise"; + +export interface PlanUI { + cta: string; + badge: string | null; + highlight: boolean; +} + +export interface PlanAddOn { + name: string; + description: string; + /** Price in cents per month */ + price_monthly: number; +} + export interface PlanConfig { + name: string; + tagline: string; + description: string; + /** Price in cents per month */ price_monthly: number; + /** Price in cents per year */ + price_yearly: number; + /** Price in cents for lifetime (null if not available) */ + price_lifetime: number | null; + /** Scans per month (null = unlimited) */ scans_per_month: number | null; - max_accounts?: number | null; + /** Resources per scan (null = unlimited) */ + resources_per_scan: number | null; + /** Maximum AWS accounts (null = unlimited) */ + aws_accounts: number | null; + /** Maximum team members (null = unlimited) */ + team_members: number | null; + /** Maximum machines (null = unlimited) */ + machines: number | null; + /** History retention in days (null = unlimited) */ + history_retention_days: number | null; + /** Feature flags - "*" means all features, "!feature" means excluded */ features: string[]; - addons?: Record; + ui: PlanUI; + addons?: Record; } export const PLANS: Record = ${JSON.stringify(plans, null, 2)} as const; -// ============================================================================ +// ============================================================================= +// Features +// ============================================================================= + +export const ALL_FEATURES = [ +${Array.from(allFeatures) + .sort() + .map((f) => ` "${f}"`) + .join(",\n")} +] as const; + +export type FeatureName = typeof ALL_FEATURES[number]; + +// ============================================================================= // Compliance Frameworks -// ============================================================================ +// ============================================================================= export const FRAMEWORK_IDS = [${frameworkIds.map((id) => `"${id}"`).join(", ")}] as const; export type FrameworkId = typeof FRAMEWORK_IDS[number]; @@ -134,9 +233,9 @@ export interface FrameworkConfig { export const COMPLIANCE_FRAMEWORKS: Record = ${JSON.stringify(frameworks.compliance_frameworks, null, 2)} as const; -// ============================================================================ +// ============================================================================= // AWS Resources -// ============================================================================ +// ============================================================================= export const RESOURCE_CATEGORIES = [${resourceCategories.map((c) => `"${c}"`).join(", ")}] as const; export type ResourceCategory = typeof RESOURCE_CATEGORIES[number]; @@ -152,14 +251,27 @@ ${Object.values(resources.aws_resources) export type AwsResourceType = typeof ALL_AWS_RESOURCES[number]; -// ============================================================================ +// ============================================================================= // Helper Functions -// ============================================================================ +// ============================================================================= export function isPlanName(value: string): value is PlanName { return PLAN_NAMES.includes(value as PlanName); } +export function isLegacyPlanName(value: string): value is LegacyPlanName { + return value in LEGACY_PLAN_MIGRATIONS; +} + +/** + * Normalize a plan name, converting legacy names to v4.0 names + */ +export function normalizePlanName(value: string): PlanName { + if (isPlanName(value)) return value; + if (isLegacyPlanName(value)) return LEGACY_PLAN_MIGRATIONS[value]; + return "community"; // Default to community for unknown plans +} + export function isFrameworkId(value: string): value is FrameworkId { return FRAMEWORK_IDS.includes(value as FrameworkId); } @@ -168,19 +280,118 @@ export function isAwsResourceType(value: string): value is AwsResourceType { return ALL_AWS_RESOURCES.includes(value as AwsResourceType); } +/** + * Check if a plan has a specific feature + */ +export function planHasFeature(plan: PlanName, feature: string): boolean { + const config = PLANS[plan]; + + // Check for "*" (all features) + if (config.features.includes("*")) { + // Check if explicitly excluded + return !config.features.includes(\`!\${feature}\`); + } + + return config.features.includes(feature); +} + +/** + * Get all features for a plan (resolving "*" and "!" modifiers) + */ export function getPlanFeatures(plan: PlanName): string[] { const config = PLANS[plan]; + if (config.features.includes("*")) { - // Return all possible features - return ["basic_scan", "graph_preview", "terraform_download", "full_audit"]; + // All features except excluded ones + const excluded = config.features + .filter(f => f.startsWith("!")) + .map(f => f.slice(1)); + return ALL_FEATURES.filter(f => !excluded.includes(f)); } - return config.features; + + return config.features.filter(f => !f.startsWith("!")); +} + +/** + * Check if a limit is unlimited (null or -1) + */ +export function isUnlimited(value: number | null): boolean { + return value === null || value === -1; +} + +/** + * Format a limit for display + */ +export function formatLimit(value: number | null): string { + if (isUnlimited(value)) return "Unlimited"; + return value!.toLocaleString(); +} + +/** + * Format price in dollars from cents + */ +export function formatPrice(cents: number): string { + if (cents === 0) return "Free"; + return \`$\${(cents / 100).toLocaleString()}\`; +} + +/** + * Get the minimum plan required for a feature + */ +export function getRequiredPlanForFeature(feature: string): PlanName { + for (const planName of PLAN_NAMES) { + if (planHasFeature(planName, feature)) { + return planName; + } + } + return "sovereign"; // Default to highest tier +} + +/** + * Get upgrade path from current plan + */ +export function getUpgradePath(currentPlan: PlanName): PlanName | null { + const upgradePaths: Record = { + community: "pro", + pro: "team", + team: "sovereign", + sovereign: null, + }; + return upgradePaths[currentPlan]; +} + +// ============================================================================= +// Plan Comparison +// ============================================================================= + +export const PLAN_RANK: Record = { + // v4.0 plans + community: 0, + pro: 1, + team: 2, + sovereign: 3, + // Legacy plans (mapped to their v4.0 equivalents) + free: 0, + solo: 1, + enterprise: 3, +}; + +export function isPlanUpgrade(from: string, to: string): boolean { + const fromRank = PLAN_RANK[normalizePlanName(from)] ?? 0; + const toRank = PLAN_RANK[normalizePlanName(to)] ?? 0; + return toRank > fromRank; +} + +export function isPlanDowngrade(from: string, to: string): boolean { + const fromRank = PLAN_RANK[normalizePlanName(from)] ?? 0; + const toRank = PLAN_RANK[normalizePlanName(to)] ?? 0; + return toRank < fromRank; } `; } function generatePython( - plans: Plans, + plans: PlansV4, frameworks: Frameworks, resources: Resources, hash: string @@ -189,19 +400,45 @@ function generatePython( const frameworkIds = Object.keys(frameworks.compliance_frameworks); const resourceCategories = Object.keys(resources.aws_resources); + // Collect all unique features + const allFeatures = new Set(); + for (const plan of Object.values(plans)) { + for (const feature of plan.features) { + if (feature !== "*" && !feature.startsWith("!")) { + allFeatures.add(feature); + } + } + } + const plansDataclass = Object.entries(plans) .map(([name, config]) => { - const addons = config.addons + const addonsStr = config.addons ? `{${Object.entries(config.addons) - .map(([k, v]) => `"${k}": ${v}`) + .map( + ([k, v]) => + `"${k}": PlanAddOn(name="${v.name}", description="${v.description}", price_monthly=${v.price_monthly})` + ) .join(", ")}}` : "None"; + + const uiStr = `PlanUI(cta="${config.ui.cta}", badge=${config.ui.badge ? `"${config.ui.badge}"` : "None"}, highlight=${config.ui.highlight ? "True" : "False"})`; + return ` "${name}": PlanConfig( + name="${config.name}", + tagline="${config.tagline}", + description="${config.description.replace(/'/g, "\\'")}", price_monthly=${config.price_monthly}, + price_yearly=${config.price_yearly}, + price_lifetime=${config.price_lifetime === null ? "None" : config.price_lifetime}, scans_per_month=${config.scans_per_month === null ? "None" : config.scans_per_month}, - max_accounts=${config.max_accounts === undefined ? "None" : config.max_accounts === null ? "None" : config.max_accounts}, + resources_per_scan=${config.resources_per_scan === null ? "None" : config.resources_per_scan}, + aws_accounts=${config.aws_accounts === null ? "None" : config.aws_accounts}, + team_members=${config.team_members === null ? "None" : config.team_members}, + machines=${config.machines === null ? "None" : config.machines}, + history_retention_days=${config.history_retention_days === null ? "None" : config.history_retention_days}, features=${JSON.stringify(config.features)}, - addons=${addons}, + ui=${uiStr}, + addons=${addonsStr}, )`; }) .join(",\n"); @@ -221,12 +458,16 @@ function generatePython( .map(([category, types]) => ` "${category}": ${JSON.stringify(types)}`) .join(",\n"); - const allResources = Object.values(resources.aws_resources).flat(); + const allResourcesList = Object.values(resources.aws_resources).flat(); return `""" -@replimap/config - Auto-generated configuration +@replimap/config v4.0 - Auto-generated configuration DO NOT EDIT - This file is generated from src/*.json +Philosophy: "Gate Output, Not Input" +- Unlimited scans for all tiers +- Charge when users export/download + Content Hash: ${hash} """ @@ -235,30 +476,63 @@ from dataclasses import dataclass from typing import Literal, Optional -# ============================================================================ +# ============================================================================= # Version -# ============================================================================ +# ============================================================================= CONFIG_VERSION: str = "${hash}" -# ============================================================================ -# Plans -# ============================================================================ +# ============================================================================= +# Plan Types +# ============================================================================= PlanName = Literal[${planNames.map((n) => `"${n}"`).join(", ")}] +LegacyPlanName = Literal["free", "solo", "enterprise"] PLAN_NAMES: tuple[PlanName, ...] = (${planNames.map((n) => `"${n}"`).join(", ")},) +LEGACY_PLAN_MIGRATIONS: dict[LegacyPlanName, PlanName] = { + "free": "community", + "solo": "pro", + "enterprise": "sovereign", +} + + +@dataclass(frozen=True) +class PlanUI: + """UI configuration for a plan.""" + cta: str + badge: Optional[str] + highlight: bool + + +@dataclass(frozen=True) +class PlanAddOn: + """Configuration for a plan add-on.""" + name: str + description: str + price_monthly: int # cents + @dataclass(frozen=True) class PlanConfig: """Configuration for a pricing plan.""" - price_monthly: int - scans_per_month: Optional[int] - max_accounts: Optional[int] + name: str + tagline: str + description: str + price_monthly: int # cents + price_yearly: int # cents + price_lifetime: Optional[int] # cents, None if not available + scans_per_month: Optional[int] # None = unlimited + resources_per_scan: Optional[int] # None = unlimited + aws_accounts: Optional[int] # None = unlimited + team_members: Optional[int] # None = unlimited + machines: Optional[int] # None = unlimited + history_retention_days: Optional[int] # None = unlimited features: list[str] - addons: Optional[dict[str, int]] = None + ui: PlanUI + addons: Optional[dict[str, PlanAddOn]] = None PLANS: dict[PlanName, PlanConfig] = { @@ -266,9 +540,26 @@ ${plansDataclass}, } -# ============================================================================ +# ============================================================================= +# Features +# ============================================================================= + +ALL_FEATURES: tuple[str, ...] = ( +${Array.from(allFeatures) + .sort() + .map((f) => ` "${f}"`) + .join(",\n")}, +) + +FeatureName = Literal[${Array.from(allFeatures) + .sort() + .map((f) => `"${f}"`) + .join(", ")}] + + +# ============================================================================= # Compliance Frameworks -# ============================================================================ +# ============================================================================= FrameworkId = Literal[${frameworkIds.map((id) => `"${id}"`).join(", ")}] @@ -289,9 +580,9 @@ ${frameworksDataclass}, } -# ============================================================================ +# ============================================================================= # AWS Resources -# ============================================================================ +# ============================================================================= ResourceCategory = Literal[${resourceCategories.map((c) => `"${c}"`).join(", ")}] @@ -302,21 +593,35 @@ ${resourcesDict}, } ALL_AWS_RESOURCES: tuple[str, ...] = ( -${allResources.map((r) => ` "${r}"`).join(",\n")}, +${allResourcesList.map((r) => ` "${r}"`).join(",\n")}, ) -AwsResourceType = Literal[${allResources.map((r) => `"${r}"`).join(", ")}] +AwsResourceType = Literal[${allResourcesList.map((r) => `"${r}"`).join(", ")}] -# ============================================================================ +# ============================================================================= # Helper Functions -# ============================================================================ +# ============================================================================= def is_plan_name(value: str) -> bool: """Check if a string is a valid plan name.""" return value in PLAN_NAMES +def is_legacy_plan_name(value: str) -> bool: + """Check if a string is a legacy plan name.""" + return value in LEGACY_PLAN_MIGRATIONS + + +def normalize_plan_name(value: str) -> PlanName: + """Normalize a plan name, converting legacy names to v4.0 names.""" + if is_plan_name(value): + return value # type: ignore + if is_legacy_plan_name(value): + return LEGACY_PLAN_MIGRATIONS[value] # type: ignore + return "community" + + def is_framework_id(value: str) -> bool: """Check if a string is a valid framework ID.""" return value in FRAMEWORK_IDS @@ -327,25 +632,111 @@ def is_aws_resource_type(value: str) -> bool: return value in ALL_AWS_RESOURCES +def plan_has_feature(plan: PlanName, feature: str) -> bool: + """Check if a plan has a specific feature.""" + config = PLANS[plan] + + # Check for "*" (all features) + if "*" in config.features: + # Check if explicitly excluded + return f"!{feature}" not in config.features + + return feature in config.features + + def get_plan_features(plan: PlanName) -> list[str]: - """Get the list of features for a plan.""" + """Get all features for a plan (resolving * and ! modifiers).""" config = PLANS[plan] + if "*" in config.features: - return ["basic_scan", "graph_preview", "terraform_download", "full_audit"] - return list(config.features) + # All features except excluded ones + excluded = [f[1:] for f in config.features if f.startswith("!")] + return [f for f in ALL_FEATURES if f not in excluded] + + return [f for f in config.features if not f.startswith("!")] + + +def is_unlimited(value: Optional[int]) -> bool: + """Check if a limit is unlimited (None or -1).""" + return value is None or value == -1 + + +def format_limit(value: Optional[int]) -> str: + """Format a limit for display.""" + if is_unlimited(value): + return "Unlimited" + return f"{value:,}" + + +def format_price(cents: int) -> str: + """Format price in dollars from cents.""" + if cents == 0: + return "Free" + return f"\${cents / 100:,.0f}" + + +def get_required_plan_for_feature(feature: str) -> PlanName: + """Get the minimum plan required for a feature.""" + for plan_name in PLAN_NAMES: + if plan_has_feature(plan_name, feature): + return plan_name + return "sovereign" + + +def get_upgrade_path(current_plan: PlanName) -> Optional[PlanName]: + """Get upgrade path from current plan.""" + upgrade_paths: dict[PlanName, Optional[PlanName]] = { + "community": "pro", + "pro": "team", + "team": "sovereign", + "sovereign": None, + } + return upgrade_paths.get(current_plan) + + +# ============================================================================= +# Plan Comparison +# ============================================================================= + +PLAN_RANK: dict[str, int] = { + # v4.0 plans + "community": 0, + "pro": 1, + "team": 2, + "sovereign": 3, + # Legacy plans + "free": 0, + "solo": 1, + "enterprise": 3, +} + + +def is_plan_upgrade(from_plan: str, to_plan: str) -> bool: + """Check if changing plans is an upgrade.""" + from_rank = PLAN_RANK.get(normalize_plan_name(from_plan), 0) + to_rank = PLAN_RANK.get(normalize_plan_name(to_plan), 0) + return to_rank > from_rank + + +def is_plan_downgrade(from_plan: str, to_plan: str) -> bool: + """Check if changing plans is a downgrade.""" + from_rank = PLAN_RANK.get(normalize_plan_name(from_plan), 0) + to_rank = PLAN_RANK.get(normalize_plan_name(to_plan), 0) + return to_rank < from_rank `; } function main(): void { const isCheck = process.argv.includes("--check"); - console.log("📦 @replimap/config code generation"); - console.log("====================================="); + console.log("📦 @replimap/config v4.0 code generation"); + console.log("========================================="); // Load JSON sources console.log("📖 Loading JSON sources..."); const { plans, frameworks, resources, combinedHash } = loadJsonFiles(); console.log(` Content hash: ${combinedHash}`); + console.log(` Plans: ${Object.keys(plans).join(", ")}`); // Generate code console.log("⚙️ Generating TypeScript..."); diff --git a/packages/config/src/plans.json b/packages/config/src/plans.json index 72e1ff5..ef3d244 100644 --- a/packages/config/src/plans.json +++ b/packages/config/src/plans.json @@ -1,30 +1,155 @@ { - "free": { + "community": { + "name": "COMMUNITY", + "tagline": "Full visibility, export when ready", + "description": "See your entire AWS infrastructure. Upgrade when you're ready to take it home.", "price_monthly": 0, - "scans_per_month": 10, - "features": ["basic_scan", "graph_preview"] + "price_yearly": 0, + "price_lifetime": null, + "scans_per_month": null, + "resources_per_scan": null, + "aws_accounts": 1, + "team_members": 1, + "machines": 1, + "history_retention_days": 7, + "features": [ + "basic_scanning", + "dependency_graph", + "cost_analysis", + "compliance_view", + "security_score", + "export_json", + "snapshot", + "snapshot_diff" + ], + "ui": { + "cta": "Get Started Free", + "badge": null, + "highlight": false + } }, "pro": { + "name": "PRO", + "tagline": "Export your infrastructure as code", + "description": "Take your Terraform code home. Perfect for individual DevOps engineers and SREs.", "price_monthly": 2900, + "price_yearly": 29000, + "price_lifetime": 19900, "scans_per_month": null, - "features": ["basic_scan", "graph_preview", "terraform_download", "full_audit"] + "resources_per_scan": null, + "aws_accounts": 3, + "team_members": 1, + "machines": 2, + "history_retention_days": 90, + "features": [ + "basic_scanning", + "dependency_graph", + "cost_analysis", + "compliance_view", + "security_score", + "export_json", + "export_terraform", + "export_csv", + "export_html", + "export_markdown", + "graph_full", + "graph_security", + "graph_export_no_watermark", + "audit_full_findings", + "audit_fix", + "audit_report_export", + "snapshot", + "snapshot_diff", + "api_access" + ], + "ui": { + "cta": "Start PRO Trial", + "badge": "Most Popular", + "highlight": true + } }, "team": { + "name": "TEAM", + "tagline": "Continuous compliance for your organization", + "description": "Drift alerts, compliance reports, and CI/CD integration for growing teams.", "price_monthly": 9900, + "price_yearly": 99000, + "price_lifetime": 49900, "scans_per_month": null, - "max_accounts": 10, - "features": ["*"] + "resources_per_scan": null, + "aws_accounts": 10, + "team_members": 5, + "machines": 10, + "history_retention_days": 365, + "features": [ + "*", + "!compliance_reports_apra", + "!compliance_reports_dora", + "!compliance_reports_essential8", + "!custom_compliance_mapping", + "!sso", + "!dedicated_support", + "!air_gap_deployment", + "!report_signature", + "!tamper_evident_audit_trail", + "!white_labeling" + ], + "ui": { + "cta": "Start TEAM Trial", + "badge": null, + "highlight": false + } }, "sovereign": { + "name": "SOVEREIGN", + "tagline": "Data sovereignty for regulated industries", + "description": "When your regulator asks 'Where does the data go?', the answer is: Nowhere.", "price_monthly": 250000, + "price_yearly": 2500000, + "price_lifetime": null, "scans_per_month": null, - "max_accounts": null, + "resources_per_scan": null, + "aws_accounts": null, + "team_members": null, + "machines": null, + "history_retention_days": null, "features": ["*"], "addons": { - "apra_cps234": 50000, - "essential_eight": 30000, - "rbnz_bs11": 40000, - "dora": 50000 + "apra_cps234": { + "name": "APRA CPS 234 Module", + "description": "Australian Prudential Regulation Authority compliance mapping", + "price_monthly": 50000 + }, + "dora": { + "name": "DORA Compliance Module", + "description": "Digital Operational Resilience Act (EU) compliance mapping", + "price_monthly": 50000 + }, + "essential_eight": { + "name": "Essential Eight Module", + "description": "Australian Cyber Security Centre maturity assessment", + "price_monthly": 30000 + }, + "rbnz_bs11": { + "name": "RBNZ BS11 Module", + "description": "Reserve Bank of New Zealand outsourcing requirements", + "price_monthly": 30000 + }, + "dedicated_am": { + "name": "Dedicated Account Manager", + "description": "Named contact for your organization", + "price_monthly": 50000 + }, + "sla_999": { + "name": "99.9% SLA Guarantee", + "description": "Uptime guarantee with financial penalties", + "price_monthly": 20000 + } + }, + "ui": { + "cta": "Request Demo", + "badge": "Sovereign Grade", + "highlight": false } } } diff --git a/packages/config/src/types/plans.ts b/packages/config/src/types/plans.ts new file mode 100644 index 0000000..3f4ee71 --- /dev/null +++ b/packages/config/src/types/plans.ts @@ -0,0 +1,220 @@ +/** + * RepliMap v4.0 Plan Type Definitions + * Philosophy: "Gate Output, Not Input" + * + * Key principle: Let users see everything, charge when they want to take it away. + */ + +// ============================================================================= +// Core Types +// ============================================================================= + +export type PlanId = 'community' | 'pro' | 'team' | 'sovereign'; +export type BillingCycle = 'monthly' | 'yearly' | 'lifetime'; +export type ExportFormat = 'json' | 'terraform' | 'csv' | 'pdf' | 'html' | 'markdown'; + +// ============================================================================= +// Plan Configuration +// ============================================================================= + +export interface PlanPricing { + /** Price in cents per month */ + monthly: number; + /** Price in cents per year */ + yearly: number; + /** One-time lifetime price in cents (null if not available) */ + lifetime: number | null; + /** Stripe price ID for monthly billing */ + stripeMonthly: string | null; + /** Stripe price ID for yearly billing */ + stripeYearly: string | null; + /** Stripe price ID for lifetime purchase */ + stripeLifetime: string | null; +} + +export interface PlanLimits { + /** Scans per month (-1 = unlimited) */ + scansPerMonth: number; + /** Resources per scan (-1 = unlimited) */ + resourcesPerScan: number; + /** Maximum AWS accounts */ + awsAccounts: number; + /** Maximum team members */ + teamMembers: number; + /** History retention in days (-1 = unlimited) */ + historyRetentionDays: number; + /** Maximum machines that can be activated */ + machines: number; +} + +export interface PlanFeatures { + // ══════════════════════════════════════════════════════════════════════════ + // VIEW (INPUT) - Mostly all true - Let users see full value + // ══════════════════════════════════════════════════════════════════════════ + basicScanning: boolean; + dependencyGraph: boolean; + costAnalysis: boolean; + complianceView: boolean; + securityScore: boolean; + + // ══════════════════════════════════════════════════════════════════════════ + // EXPORT (OUTPUT) - Core monetization layer + // ══════════════════════════════════════════════════════════════════════════ + exportJson: boolean; + /** Include upgrade prompts in JSON export for free users */ + exportJsonMetadata: boolean; + exportTerraform: boolean; + exportCsv: boolean; + exportPdf: boolean; + exportHtml: boolean; + exportMarkdown: boolean; + /** true = has watermark on exports */ + graphExportWatermark: boolean; + + // ══════════════════════════════════════════════════════════════════════════ + // GRAPH MODES + // ══════════════════════════════════════════════════════════════════════════ + graphFull: boolean; + graphSecurity: boolean; + + // ══════════════════════════════════════════════════════════════════════════ + // AUDIT FEATURES + // ══════════════════════════════════════════════════════════════════════════ + auditFullFindings: boolean; + auditFix: boolean; + auditReportExport: boolean; + auditCiMode: boolean; + + // ══════════════════════════════════════════════════════════════════════════ + // COMPLIANCE - Tiered unlocking + // ══════════════════════════════════════════════════════════════════════════ + complianceReportsCis: boolean; + complianceReportsSoc2: boolean; + complianceReportsApra: boolean; + complianceReportsDora: boolean; + complianceReportsEssential8: boolean; + customComplianceMapping: boolean; + + // ══════════════════════════════════════════════════════════════════════════ + // AUTOMATION + // ══════════════════════════════════════════════════════════════════════════ + apiAccess: boolean; + customRules: boolean; + driftDetection: boolean; + driftWatch: boolean; + cicdIntegration: boolean; + + // ══════════════════════════════════════════════════════════════════════════ + // ADVANCED ANALYSIS + // ══════════════════════════════════════════════════════════════════════════ + deps: boolean; + depsExport: boolean; + cost: boolean; + costExport: boolean; + snapshot: boolean; + snapshotDiff: boolean; + + // ══════════════════════════════════════════════════════════════════════════ + // TEAM COLLABORATION - TEAM differentiator + // ══════════════════════════════════════════════════════════════════════════ + driftAlertsSlack: boolean; + driftAlertsTeams: boolean; + driftAlertsWebhook: boolean; + multiAccount: boolean; + teamCollaboration: boolean; + + // ══════════════════════════════════════════════════════════════════════════ + // ENTERPRISE SECURITY - SOVEREIGN exclusive + // ══════════════════════════════════════════════════════════════════════════ + sso: boolean; + dedicatedSupport: boolean; + airGapDeployment: boolean; + /** SHA256 signed reports */ + reportSignature: boolean; + tamperEvidentAuditTrail: boolean; + whiteLabeling: boolean; + + // ══════════════════════════════════════════════════════════════════════════ + // SUPPORT + // ══════════════════════════════════════════════════════════════════════════ + prioritySupport: boolean; + /** SLA response time in hours (null = no SLA) */ + supportSlaHours: number | null; +} + +export interface PlanUI { + /** Call-to-action button text */ + cta: string; + /** Badge to display (e.g., "Most Popular") */ + badge: string | null; + /** Whether to visually highlight this plan */ + highlight: boolean; +} + +export interface PlanAddOn { + id: string; + name: string; + description: string; + /** Price in cents per month */ + priceMonthly: number; +} + +export interface PlanConfig { + id: PlanId; + name: string; + tagline: string; + description: string; + pricing: PlanPricing; + limits: PlanLimits; + features: PlanFeatures; + ui: PlanUI; + addOns?: PlanAddOn[]; +} + +// ============================================================================= +// Legacy Plan Migration +// ============================================================================= + +/** Maps old plan names to new v4.0 plan IDs */ +export const LEGACY_PLAN_MIGRATIONS: Record = { + free: 'community', + solo: 'pro', // Solo users get upgraded to PRO + enterprise: 'sovereign', +} as const; + +/** Old plan names that need migration */ +export const LEGACY_PLAN_NAMES = ['free', 'solo', 'enterprise'] as const; +export type LegacyPlanName = typeof LEGACY_PLAN_NAMES[number]; + +// ============================================================================= +// Upgrade Prompts +// ============================================================================= + +export interface UpgradePrompt { + id: string; + title: string; + message: string; + currentPlan: PlanId; + requiredPlan: PlanId; + price: string; + cta: string; + ctaUrl: string; + benefits: string[]; +} + +// ============================================================================= +// JSON Export Metadata +// ============================================================================= + +export interface JsonExportMetadata { + generator: string; + version: string; + planId: string; + exportedAt: string; + upgradeUrl: string; + upgradeNote: string; + features: { + available: string[]; + locked: string[]; + }; +}