guardrail-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/autopilot.test.d.ts +7 -0
- package/dist/__tests__/autopilot.test.d.ts.map +1 -0
- package/dist/__tests__/autopilot.test.js +156 -0
- package/dist/__tests__/tier-config.test.d.ts +9 -0
- package/dist/__tests__/tier-config.test.d.ts.map +1 -0
- package/dist/__tests__/tier-config.test.js +230 -0
- package/dist/__tests__/utils/hash-inline.test.d.ts +2 -0
- package/dist/__tests__/utils/hash-inline.test.d.ts.map +1 -0
- package/dist/__tests__/utils/hash-inline.test.js +62 -0
- package/dist/__tests__/utils/hash.test.d.ts +3 -0
- package/dist/__tests__/utils/hash.test.d.ts.map +1 -0
- package/dist/__tests__/utils/hash.test.js +95 -0
- package/dist/__tests__/utils/simple.test.d.ts +1 -0
- package/dist/__tests__/utils/simple.test.d.ts.map +1 -0
- package/dist/__tests__/utils/simple.test.js +10 -0
- package/dist/__tests__/utils/utils-simple.test.d.ts +1 -0
- package/dist/__tests__/utils/utils-simple.test.d.ts.map +1 -0
- package/dist/__tests__/utils/utils-simple.test.js +6 -0
- package/dist/__tests__/utils/utils.test.d.ts +15 -0
- package/dist/__tests__/utils/utils.test.d.ts.map +1 -0
- package/dist/__tests__/utils/utils.test.js +172 -0
- package/dist/autopilot/autopilot-runner.d.ts +33 -0
- package/dist/autopilot/autopilot-runner.d.ts.map +1 -0
- package/dist/autopilot/autopilot-runner.js +479 -0
- package/dist/autopilot/index.d.ts +6 -0
- package/dist/autopilot/index.d.ts.map +1 -0
- package/dist/autopilot/index.js +25 -0
- package/dist/autopilot/types.d.ts +102 -0
- package/dist/autopilot/types.d.ts.map +1 -0
- package/dist/autopilot/types.js +18 -0
- package/dist/cache/index.d.ts +7 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +22 -0
- package/dist/cache/redis-cache.d.ts +145 -0
- package/dist/cache/redis-cache.d.ts.map +1 -0
- package/dist/cache/redis-cache.js +459 -0
- package/dist/ci/github-actions.d.ts +77 -0
- package/dist/ci/github-actions.d.ts.map +1 -0
- package/dist/ci/github-actions.js +277 -0
- package/dist/ci/index.d.ts +12 -0
- package/dist/ci/index.d.ts.map +1 -0
- package/dist/ci/index.js +27 -0
- package/dist/ci/pre-commit.d.ts +65 -0
- package/dist/ci/pre-commit.d.ts.map +1 -0
- package/dist/ci/pre-commit.js +286 -0
- package/dist/entitlements.d.ts +149 -0
- package/dist/entitlements.d.ts.map +1 -0
- package/dist/entitlements.js +464 -0
- package/dist/env.d.ts +113 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +204 -0
- package/dist/fix-packs/__tests__/generate-fix-packs.test.d.ts +7 -0
- package/dist/fix-packs/__tests__/generate-fix-packs.test.d.ts.map +1 -0
- package/dist/fix-packs/__tests__/generate-fix-packs.test.js +250 -0
- package/dist/fix-packs/generate-fix-packs.d.ts +15 -0
- package/dist/fix-packs/generate-fix-packs.d.ts.map +1 -0
- package/dist/fix-packs/generate-fix-packs.js +505 -0
- package/dist/fix-packs/index.d.ts +8 -0
- package/dist/fix-packs/index.d.ts.map +1 -0
- package/dist/fix-packs/index.js +23 -0
- package/dist/fix-packs/types.d.ts +113 -0
- package/dist/fix-packs/types.d.ts.map +1 -0
- package/dist/fix-packs/types.js +71 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/metrics/prometheus.d.ts +99 -0
- package/dist/metrics/prometheus.d.ts.map +1 -0
- package/dist/metrics/prometheus.js +306 -0
- package/dist/quota-ledger.d.ts +119 -0
- package/dist/quota-ledger.d.ts.map +1 -0
- package/dist/quota-ledger.js +462 -0
- package/dist/rbac/__tests__/permissions.test.d.ts +8 -0
- package/dist/rbac/__tests__/permissions.test.d.ts.map +1 -0
- package/dist/rbac/__tests__/permissions.test.js +350 -0
- package/dist/rbac/index.d.ts +9 -0
- package/dist/rbac/index.d.ts.map +1 -0
- package/dist/rbac/index.js +32 -0
- package/dist/rbac/permissions.d.ts +71 -0
- package/dist/rbac/permissions.d.ts.map +1 -0
- package/dist/rbac/permissions.js +247 -0
- package/dist/rbac/types.d.ts +69 -0
- package/dist/rbac/types.d.ts.map +1 -0
- package/dist/rbac/types.js +213 -0
- package/dist/tier-config.d.ts +203 -0
- package/dist/tier-config.d.ts.map +1 -0
- package/dist/tier-config.js +675 -0
- package/dist/types.d.ts +365 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/utils.d.ts +36 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +127 -0
- package/dist/verified-autofix/__tests__/format-validator.test.d.ts +11 -0
- package/dist/verified-autofix/__tests__/format-validator.test.d.ts.map +1 -0
- package/dist/verified-autofix/__tests__/format-validator.test.js +285 -0
- package/dist/verified-autofix/__tests__/pipeline.test.d.ts +11 -0
- package/dist/verified-autofix/__tests__/pipeline.test.d.ts.map +1 -0
- package/dist/verified-autofix/__tests__/pipeline.test.js +389 -0
- package/dist/verified-autofix/__tests__/repo-fingerprint.test.d.ts +11 -0
- package/dist/verified-autofix/__tests__/repo-fingerprint.test.d.ts.map +1 -0
- package/dist/verified-autofix/__tests__/repo-fingerprint.test.js +236 -0
- package/dist/verified-autofix/__tests__/workspace.test.d.ts +11 -0
- package/dist/verified-autofix/__tests__/workspace.test.d.ts.map +1 -0
- package/dist/verified-autofix/__tests__/workspace.test.js +314 -0
- package/dist/verified-autofix/format-validator.d.ts +101 -0
- package/dist/verified-autofix/format-validator.d.ts.map +1 -0
- package/dist/verified-autofix/format-validator.js +446 -0
- package/dist/verified-autofix/index.d.ts +14 -0
- package/dist/verified-autofix/index.d.ts.map +1 -0
- package/dist/verified-autofix/index.js +39 -0
- package/dist/verified-autofix/pipeline.d.ts +68 -0
- package/dist/verified-autofix/pipeline.d.ts.map +1 -0
- package/dist/verified-autofix/pipeline.js +330 -0
- package/dist/verified-autofix/repo-fingerprint.d.ts +56 -0
- package/dist/verified-autofix/repo-fingerprint.d.ts.map +1 -0
- package/dist/verified-autofix/repo-fingerprint.js +396 -0
- package/dist/verified-autofix/workspace.d.ts +83 -0
- package/dist/verified-autofix/workspace.d.ts.map +1 -0
- package/dist/verified-autofix/workspace.js +454 -0
- package/dist/verified-autofix.d.ts +182 -0
- package/dist/verified-autofix.d.ts.map +1 -0
- package/dist/verified-autofix.js +1021 -0
- package/dist/visualization/dependency-graph.d.ts +79 -0
- package/dist/visualization/dependency-graph.d.ts.map +1 -0
- package/dist/visualization/dependency-graph.js +399 -0
- package/dist/visualization/index.d.ts +5 -0
- package/dist/visualization/index.d.ts.map +1 -0
- package/dist/visualization/index.js +20 -0
- package/package.json +29 -0
- package/src/__tests__/autopilot.test.ts +196 -0
- package/src/__tests__/tier-config.test.ts +289 -0
- package/src/__tests__/utils/hash-inline.test.ts +76 -0
- package/src/__tests__/utils/hash.test.ts +119 -0
- package/src/__tests__/utils/simple.test.ts +10 -0
- package/src/__tests__/utils/utils-simple.test.ts +5 -0
- package/src/__tests__/utils/utils.test.ts +203 -0
- package/src/autopilot/autopilot-runner.ts +503 -0
- package/src/autopilot/index.ts +6 -0
- package/src/autopilot/types.ts +119 -0
- package/src/cache/index.ts +7 -0
- package/src/cache/redis-cache.d.ts +155 -0
- package/src/cache/redis-cache.d.ts.map +1 -0
- package/src/cache/redis-cache.ts +517 -0
- package/src/ci/github-actions.ts +335 -0
- package/src/ci/index.ts +12 -0
- package/src/ci/pre-commit.ts +338 -0
- package/src/db/usage-schema.prisma +114 -0
- package/src/entitlements.ts +570 -0
- package/src/env.d.ts +68 -0
- package/src/env.d.ts.map +1 -0
- package/src/env.ts +247 -0
- package/src/fix-packs/__tests__/generate-fix-packs.test.ts +317 -0
- package/src/fix-packs/generate-fix-packs.ts +577 -0
- package/src/fix-packs/index.ts +8 -0
- package/src/fix-packs/types.ts +206 -0
- package/src/index.d.ts +7 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.ts +12 -0
- package/src/metrics/prometheus.d.ts +104 -0
- package/src/metrics/prometheus.d.ts.map +1 -0
- package/src/metrics/prometheus.ts +446 -0
- package/src/quota-ledger.ts +548 -0
- package/src/rbac/__tests__/permissions.test.ts +446 -0
- package/src/rbac/index.ts +46 -0
- package/src/rbac/permissions.ts +301 -0
- package/src/rbac/types.ts +298 -0
- package/src/tier-config.json +157 -0
- package/src/tier-config.ts +815 -0
- package/src/types.d.ts +365 -0
- package/src/types.d.ts.map +1 -0
- package/src/types.ts +441 -0
- package/src/utils.d.ts +36 -0
- package/src/utils.d.ts.map +1 -0
- package/src/utils.ts +140 -0
- package/src/verified-autofix/__tests__/format-validator.test.ts +335 -0
- package/src/verified-autofix/__tests__/pipeline.test.ts +419 -0
- package/src/verified-autofix/__tests__/repo-fingerprint.test.ts +241 -0
- package/src/verified-autofix/__tests__/workspace.test.ts +373 -0
- package/src/verified-autofix/format-validator.ts +517 -0
- package/src/verified-autofix/index.ts +63 -0
- package/src/verified-autofix/pipeline.ts +403 -0
- package/src/verified-autofix/repo-fingerprint.ts +459 -0
- package/src/verified-autofix/workspace.ts +531 -0
- package/src/verified-autofix.ts +1187 -0
- package/src/visualization/dependency-graph.d.ts +85 -0
- package/src/visualization/dependency-graph.d.ts.map +1 -0
- package/src/visualization/dependency-graph.ts +495 -0
- package/src/visualization/index.ts +5 -0
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical Tier Configuration
|
|
3
|
+
*
|
|
4
|
+
* SINGLE SOURCE OF TRUTH for all tier definitions across:
|
|
5
|
+
* - Backend entitlements
|
|
6
|
+
* - Rate limiting
|
|
7
|
+
* - Stripe billing mapping
|
|
8
|
+
* - Landing page pricing
|
|
9
|
+
* - CLI entitlements
|
|
10
|
+
*
|
|
11
|
+
* DO NOT define tier configurations anywhere else in the codebase.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// TIER ENUM
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
export const TIERS = ['free', 'starter', 'pro', 'compliance', 'enterprise', 'unlimited'] as const;
|
|
19
|
+
export type Tier = typeof TIERS[number];
|
|
20
|
+
|
|
21
|
+
/** Tiers that can be purchased (excludes free and unlimited) */
|
|
22
|
+
export const PURCHASABLE_TIERS = ['starter', 'pro', 'compliance', 'enterprise'] as const;
|
|
23
|
+
export type PurchasableTier = typeof PURCHASABLE_TIERS[number];
|
|
24
|
+
|
|
25
|
+
/** Tier order for comparison (lower index = lower tier) */
|
|
26
|
+
export const TIER_ORDER: Tier[] = ['free', 'starter', 'pro', 'compliance', 'enterprise', 'unlimited'];
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// FEATURE FLAGS
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
export const FEATURES = [
|
|
33
|
+
'scan',
|
|
34
|
+
'scan:full',
|
|
35
|
+
'scan:security',
|
|
36
|
+
'scan:compliance',
|
|
37
|
+
'gate',
|
|
38
|
+
'fix',
|
|
39
|
+
'fix:auto',
|
|
40
|
+
'ship',
|
|
41
|
+
'reality',
|
|
42
|
+
'reality:flows',
|
|
43
|
+
'ai-agent',
|
|
44
|
+
'ai-agent:goals',
|
|
45
|
+
'autopilot',
|
|
46
|
+
'context',
|
|
47
|
+
'badge',
|
|
48
|
+
'mcp',
|
|
49
|
+
'compliance:soc2',
|
|
50
|
+
'compliance:hipaa',
|
|
51
|
+
'compliance:gdpr',
|
|
52
|
+
'compliance:pci',
|
|
53
|
+
'compliance:nist',
|
|
54
|
+
'compliance:iso27001',
|
|
55
|
+
'reports:html',
|
|
56
|
+
'reports:pdf',
|
|
57
|
+
'reports:sarif',
|
|
58
|
+
'team:members',
|
|
59
|
+
'team:admin',
|
|
60
|
+
'api:access',
|
|
61
|
+
'webhooks',
|
|
62
|
+
'deploy-hooks',
|
|
63
|
+
] as const;
|
|
64
|
+
|
|
65
|
+
export type Feature = typeof FEATURES[number];
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// TIER CONFIGURATION INTERFACE
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
export interface TierLimits {
|
|
72
|
+
/** Scans per month (-1 = unlimited) */
|
|
73
|
+
scansPerMonth: number;
|
|
74
|
+
/** Reality Mode runs per month */
|
|
75
|
+
realityRunsPerMonth: number;
|
|
76
|
+
/** AI Agent runs per month */
|
|
77
|
+
aiAgentRunsPerMonth: number;
|
|
78
|
+
/** Number of projects */
|
|
79
|
+
projects: number;
|
|
80
|
+
/** Base team members included in tier */
|
|
81
|
+
teamMembers: number;
|
|
82
|
+
/** Compliance frameworks available */
|
|
83
|
+
complianceFrameworks: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ============================================================================
|
|
87
|
+
// SEAT PRICING CONFIGURATION
|
|
88
|
+
// ============================================================================
|
|
89
|
+
|
|
90
|
+
export interface SeatPricing {
|
|
91
|
+
/** Price per additional seat per month */
|
|
92
|
+
monthlyPricePerSeat: number;
|
|
93
|
+
/** Price per additional seat per year */
|
|
94
|
+
annualPricePerSeat: number;
|
|
95
|
+
/** Maximum additional seats allowed (-1 = unlimited) */
|
|
96
|
+
maxAdditionalSeats: number;
|
|
97
|
+
/** Whether this tier supports additional seats */
|
|
98
|
+
supportsAdditionalSeats: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const SEAT_PRICING: Record<Tier, SeatPricing> = {
|
|
102
|
+
free: {
|
|
103
|
+
monthlyPricePerSeat: 0,
|
|
104
|
+
annualPricePerSeat: 0,
|
|
105
|
+
maxAdditionalSeats: 0,
|
|
106
|
+
supportsAdditionalSeats: false,
|
|
107
|
+
},
|
|
108
|
+
starter: {
|
|
109
|
+
monthlyPricePerSeat: 0,
|
|
110
|
+
annualPricePerSeat: 0,
|
|
111
|
+
maxAdditionalSeats: 0,
|
|
112
|
+
supportsAdditionalSeats: false,
|
|
113
|
+
},
|
|
114
|
+
pro: {
|
|
115
|
+
monthlyPricePerSeat: 25,
|
|
116
|
+
annualPricePerSeat: 240, // 20% off: 25 * 12 * 0.8 = 240
|
|
117
|
+
maxAdditionalSeats: 45, // Base 5 + max 45 = 50 total
|
|
118
|
+
supportsAdditionalSeats: true,
|
|
119
|
+
},
|
|
120
|
+
compliance: {
|
|
121
|
+
monthlyPricePerSeat: 35,
|
|
122
|
+
annualPricePerSeat: 336, // 20% off: 35 * 12 * 0.8 = 336
|
|
123
|
+
maxAdditionalSeats: 90, // Base 10 + max 90 = 100 total
|
|
124
|
+
supportsAdditionalSeats: true,
|
|
125
|
+
},
|
|
126
|
+
enterprise: {
|
|
127
|
+
monthlyPricePerSeat: 45,
|
|
128
|
+
annualPricePerSeat: 432, // 20% off: 45 * 12 * 0.8 = 432
|
|
129
|
+
maxAdditionalSeats: -1, // Unlimited
|
|
130
|
+
supportsAdditionalSeats: true,
|
|
131
|
+
},
|
|
132
|
+
unlimited: {
|
|
133
|
+
monthlyPricePerSeat: 0,
|
|
134
|
+
annualPricePerSeat: 0,
|
|
135
|
+
maxAdditionalSeats: -1,
|
|
136
|
+
supportsAdditionalSeats: true,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export interface RateLimitConfig {
|
|
141
|
+
/** Requests per minute */
|
|
142
|
+
requestsPerMinute: number;
|
|
143
|
+
/** Burst limit (max requests in short window) */
|
|
144
|
+
burstLimit: number;
|
|
145
|
+
/** Rate limit window in milliseconds */
|
|
146
|
+
windowMs: number;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface TierConfig {
|
|
150
|
+
/** Tier identifier */
|
|
151
|
+
id: Tier;
|
|
152
|
+
/** Display name */
|
|
153
|
+
name: string;
|
|
154
|
+
/** Monthly price in USD */
|
|
155
|
+
price: number;
|
|
156
|
+
/** Annual price in USD (typically ~2 months free) */
|
|
157
|
+
annualPrice: number;
|
|
158
|
+
/** Short description for pricing page */
|
|
159
|
+
description: string;
|
|
160
|
+
/** Features included in this tier */
|
|
161
|
+
features: Feature[];
|
|
162
|
+
/** Usage limits */
|
|
163
|
+
limits: TierLimits;
|
|
164
|
+
/** API rate limiting configuration */
|
|
165
|
+
rateLimit: RateLimitConfig;
|
|
166
|
+
/** Upsell configuration */
|
|
167
|
+
upsell: {
|
|
168
|
+
message: string;
|
|
169
|
+
nextTier: Tier;
|
|
170
|
+
};
|
|
171
|
+
/** Stripe price IDs (set via environment variables) */
|
|
172
|
+
stripe?: {
|
|
173
|
+
monthlyPriceId?: string;
|
|
174
|
+
annualPriceId?: string;
|
|
175
|
+
/** Stripe price ID for additional seats (metered or per-unit) */
|
|
176
|
+
seatPriceId?: string;
|
|
177
|
+
seatAnnualPriceId?: string;
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ============================================================================
|
|
182
|
+
// CANONICAL TIER DEFINITIONS
|
|
183
|
+
// ============================================================================
|
|
184
|
+
|
|
185
|
+
export const TIER_CONFIG: Record<Tier, TierConfig> = {
|
|
186
|
+
free: {
|
|
187
|
+
id: 'free',
|
|
188
|
+
name: 'Free',
|
|
189
|
+
price: 0,
|
|
190
|
+
annualPrice: 0,
|
|
191
|
+
description: 'Get started',
|
|
192
|
+
features: [
|
|
193
|
+
'scan',
|
|
194
|
+
'gate',
|
|
195
|
+
'ship',
|
|
196
|
+
'context',
|
|
197
|
+
'badge',
|
|
198
|
+
],
|
|
199
|
+
limits: {
|
|
200
|
+
scansPerMonth: 10,
|
|
201
|
+
realityRunsPerMonth: 0,
|
|
202
|
+
aiAgentRunsPerMonth: 0,
|
|
203
|
+
projects: 1,
|
|
204
|
+
teamMembers: 1,
|
|
205
|
+
complianceFrameworks: 0,
|
|
206
|
+
},
|
|
207
|
+
rateLimit: {
|
|
208
|
+
requestsPerMinute: 100,
|
|
209
|
+
burstLimit: 150,
|
|
210
|
+
windowMs: 60 * 1000,
|
|
211
|
+
},
|
|
212
|
+
upsell: {
|
|
213
|
+
message: 'Upgrade to Starter for Reality Mode browser testing and 100 scans/month',
|
|
214
|
+
nextTier: 'starter',
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
starter: {
|
|
219
|
+
id: 'starter',
|
|
220
|
+
name: 'Starter',
|
|
221
|
+
price: 29,
|
|
222
|
+
annualPrice: 278, // 20% off: 29 * 12 * 0.8 = 278.40 → 278
|
|
223
|
+
description: 'For solo devs',
|
|
224
|
+
features: [
|
|
225
|
+
'scan',
|
|
226
|
+
'scan:full',
|
|
227
|
+
'gate',
|
|
228
|
+
'fix',
|
|
229
|
+
'ship',
|
|
230
|
+
'reality',
|
|
231
|
+
'context',
|
|
232
|
+
'badge',
|
|
233
|
+
'reports:html',
|
|
234
|
+
],
|
|
235
|
+
limits: {
|
|
236
|
+
scansPerMonth: 100,
|
|
237
|
+
realityRunsPerMonth: 20,
|
|
238
|
+
aiAgentRunsPerMonth: 0,
|
|
239
|
+
projects: 3,
|
|
240
|
+
teamMembers: 1,
|
|
241
|
+
complianceFrameworks: 0,
|
|
242
|
+
},
|
|
243
|
+
rateLimit: {
|
|
244
|
+
requestsPerMinute: 300,
|
|
245
|
+
burstLimit: 450,
|
|
246
|
+
windowMs: 60 * 1000,
|
|
247
|
+
},
|
|
248
|
+
upsell: {
|
|
249
|
+
message: 'Upgrade to Pro for AI Agent testing, auto-fix, and Autopilot protection',
|
|
250
|
+
nextTier: 'pro',
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
pro: {
|
|
255
|
+
id: 'pro',
|
|
256
|
+
name: 'Pro',
|
|
257
|
+
price: 99,
|
|
258
|
+
annualPrice: 950, // 20% off: 99 * 12 * 0.8 = 950.40 → 950
|
|
259
|
+
description: 'Full automation',
|
|
260
|
+
features: [
|
|
261
|
+
'scan',
|
|
262
|
+
'scan:full',
|
|
263
|
+
'scan:security',
|
|
264
|
+
'gate',
|
|
265
|
+
'fix',
|
|
266
|
+
'fix:auto',
|
|
267
|
+
'ship',
|
|
268
|
+
'reality',
|
|
269
|
+
'reality:flows',
|
|
270
|
+
'ai-agent',
|
|
271
|
+
'ai-agent:goals',
|
|
272
|
+
'autopilot',
|
|
273
|
+
'context',
|
|
274
|
+
'badge',
|
|
275
|
+
'mcp',
|
|
276
|
+
'reports:html',
|
|
277
|
+
'reports:sarif',
|
|
278
|
+
'api:access',
|
|
279
|
+
'webhooks',
|
|
280
|
+
],
|
|
281
|
+
limits: {
|
|
282
|
+
scansPerMonth: 500,
|
|
283
|
+
realityRunsPerMonth: 100,
|
|
284
|
+
aiAgentRunsPerMonth: 50,
|
|
285
|
+
projects: 10,
|
|
286
|
+
teamMembers: 5,
|
|
287
|
+
complianceFrameworks: 0,
|
|
288
|
+
},
|
|
289
|
+
rateLimit: {
|
|
290
|
+
requestsPerMinute: 1000,
|
|
291
|
+
burstLimit: 1500,
|
|
292
|
+
windowMs: 60 * 1000,
|
|
293
|
+
},
|
|
294
|
+
upsell: {
|
|
295
|
+
message: 'Upgrade to Compliance tier for SOC2, HIPAA, GDPR frameworks',
|
|
296
|
+
nextTier: 'compliance',
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
compliance: {
|
|
301
|
+
id: 'compliance',
|
|
302
|
+
name: 'Compliance',
|
|
303
|
+
price: 199,
|
|
304
|
+
annualPrice: 1910, // 20% off: 199 * 12 * 0.8 = 1910.40 → 1910
|
|
305
|
+
description: 'Enterprise ready',
|
|
306
|
+
features: [
|
|
307
|
+
'scan',
|
|
308
|
+
'scan:full',
|
|
309
|
+
'scan:security',
|
|
310
|
+
'scan:compliance',
|
|
311
|
+
'gate',
|
|
312
|
+
'fix',
|
|
313
|
+
'fix:auto',
|
|
314
|
+
'ship',
|
|
315
|
+
'reality',
|
|
316
|
+
'reality:flows',
|
|
317
|
+
'ai-agent',
|
|
318
|
+
'ai-agent:goals',
|
|
319
|
+
'autopilot',
|
|
320
|
+
'context',
|
|
321
|
+
'badge',
|
|
322
|
+
'mcp',
|
|
323
|
+
'compliance:soc2',
|
|
324
|
+
'compliance:hipaa',
|
|
325
|
+
'compliance:gdpr',
|
|
326
|
+
'compliance:pci',
|
|
327
|
+
'compliance:nist',
|
|
328
|
+
'compliance:iso27001',
|
|
329
|
+
'reports:html',
|
|
330
|
+
'reports:pdf',
|
|
331
|
+
'reports:sarif',
|
|
332
|
+
'api:access',
|
|
333
|
+
'webhooks',
|
|
334
|
+
'deploy-hooks',
|
|
335
|
+
],
|
|
336
|
+
limits: {
|
|
337
|
+
scansPerMonth: 1000,
|
|
338
|
+
realityRunsPerMonth: 200,
|
|
339
|
+
aiAgentRunsPerMonth: 100,
|
|
340
|
+
projects: 25,
|
|
341
|
+
teamMembers: 10,
|
|
342
|
+
complianceFrameworks: 6,
|
|
343
|
+
},
|
|
344
|
+
rateLimit: {
|
|
345
|
+
requestsPerMinute: 2000,
|
|
346
|
+
burstLimit: 3000,
|
|
347
|
+
windowMs: 60 * 1000,
|
|
348
|
+
},
|
|
349
|
+
upsell: {
|
|
350
|
+
message: 'Contact sales for Enterprise with unlimited usage and dedicated support',
|
|
351
|
+
nextTier: 'enterprise',
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
enterprise: {
|
|
356
|
+
id: 'enterprise',
|
|
357
|
+
name: 'Enterprise',
|
|
358
|
+
price: 499,
|
|
359
|
+
annualPrice: 4790, // 20% off: 499 * 12 * 0.8 = 4790.40 → 4790
|
|
360
|
+
description: 'Custom solutions',
|
|
361
|
+
features: [
|
|
362
|
+
'scan',
|
|
363
|
+
'scan:full',
|
|
364
|
+
'scan:security',
|
|
365
|
+
'scan:compliance',
|
|
366
|
+
'gate',
|
|
367
|
+
'fix',
|
|
368
|
+
'fix:auto',
|
|
369
|
+
'ship',
|
|
370
|
+
'reality',
|
|
371
|
+
'reality:flows',
|
|
372
|
+
'ai-agent',
|
|
373
|
+
'ai-agent:goals',
|
|
374
|
+
'autopilot',
|
|
375
|
+
'context',
|
|
376
|
+
'badge',
|
|
377
|
+
'mcp',
|
|
378
|
+
'compliance:soc2',
|
|
379
|
+
'compliance:hipaa',
|
|
380
|
+
'compliance:gdpr',
|
|
381
|
+
'compliance:pci',
|
|
382
|
+
'compliance:nist',
|
|
383
|
+
'compliance:iso27001',
|
|
384
|
+
'reports:html',
|
|
385
|
+
'reports:pdf',
|
|
386
|
+
'reports:sarif',
|
|
387
|
+
'api:access',
|
|
388
|
+
'webhooks',
|
|
389
|
+
'deploy-hooks',
|
|
390
|
+
'team:members',
|
|
391
|
+
'team:admin',
|
|
392
|
+
],
|
|
393
|
+
limits: {
|
|
394
|
+
scansPerMonth: 5000,
|
|
395
|
+
realityRunsPerMonth: 1000,
|
|
396
|
+
aiAgentRunsPerMonth: 500,
|
|
397
|
+
projects: 100,
|
|
398
|
+
teamMembers: 50,
|
|
399
|
+
complianceFrameworks: 6,
|
|
400
|
+
},
|
|
401
|
+
rateLimit: {
|
|
402
|
+
requestsPerMinute: 10000,
|
|
403
|
+
burstLimit: 15000,
|
|
404
|
+
windowMs: 60 * 1000,
|
|
405
|
+
},
|
|
406
|
+
upsell: {
|
|
407
|
+
message: 'You have our top tier! Contact support for custom requirements.',
|
|
408
|
+
nextTier: 'unlimited',
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
unlimited: {
|
|
413
|
+
id: 'unlimited',
|
|
414
|
+
name: 'Unlimited',
|
|
415
|
+
price: 0,
|
|
416
|
+
annualPrice: 0,
|
|
417
|
+
description: 'Internal/Special',
|
|
418
|
+
features: FEATURES as unknown as Feature[],
|
|
419
|
+
limits: {
|
|
420
|
+
scansPerMonth: -1, // Unlimited
|
|
421
|
+
realityRunsPerMonth: -1,
|
|
422
|
+
aiAgentRunsPerMonth: -1,
|
|
423
|
+
projects: -1,
|
|
424
|
+
teamMembers: -1,
|
|
425
|
+
complianceFrameworks: 6,
|
|
426
|
+
},
|
|
427
|
+
rateLimit: {
|
|
428
|
+
requestsPerMinute: 100000,
|
|
429
|
+
burstLimit: 150000,
|
|
430
|
+
windowMs: 60 * 1000,
|
|
431
|
+
},
|
|
432
|
+
upsell: {
|
|
433
|
+
message: 'You have unlimited access!',
|
|
434
|
+
nextTier: 'unlimited',
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// ============================================================================
|
|
440
|
+
// HELPER FUNCTIONS
|
|
441
|
+
// ============================================================================
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Check if a tier string is valid
|
|
445
|
+
*/
|
|
446
|
+
export function isValidTier(tier: string): tier is Tier {
|
|
447
|
+
return TIERS.includes(tier as Tier);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Get tier config by tier name
|
|
452
|
+
*/
|
|
453
|
+
export function getTierConfig(tier: Tier): TierConfig {
|
|
454
|
+
return TIER_CONFIG[tier];
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Get all tier configs as array (useful for iteration)
|
|
459
|
+
*/
|
|
460
|
+
export function getAllTierConfigs(): TierConfig[] {
|
|
461
|
+
return TIER_ORDER.map(tier => TIER_CONFIG[tier]);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Get purchasable tier configs (for pricing page)
|
|
466
|
+
*/
|
|
467
|
+
export function getPurchasableTierConfigs(): TierConfig[] {
|
|
468
|
+
return PURCHASABLE_TIERS.map(tier => TIER_CONFIG[tier]);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Compare two tiers (-1 if a < b, 0 if equal, 1 if a > b)
|
|
473
|
+
*/
|
|
474
|
+
export function compareTiers(a: Tier, b: Tier): number {
|
|
475
|
+
const indexA = TIER_ORDER.indexOf(a);
|
|
476
|
+
const indexB = TIER_ORDER.indexOf(b);
|
|
477
|
+
return indexA - indexB;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Check if tier A is higher than tier B
|
|
482
|
+
*/
|
|
483
|
+
export function isTierHigher(a: Tier, b: Tier): boolean {
|
|
484
|
+
return compareTiers(a, b) > 0;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Find the minimum tier that has a specific feature
|
|
489
|
+
*/
|
|
490
|
+
export function getMinimumTierForFeature(feature: Feature): Tier | null {
|
|
491
|
+
for (const tier of TIER_ORDER) {
|
|
492
|
+
if (TIER_CONFIG[tier].features.includes(feature)) {
|
|
493
|
+
return tier;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Check if a tier has a specific feature
|
|
501
|
+
*/
|
|
502
|
+
export function tierHasFeature(tier: Tier, feature: Feature): boolean {
|
|
503
|
+
const config = TIER_CONFIG[tier];
|
|
504
|
+
// Unlimited tier has all features
|
|
505
|
+
if (tier === 'unlimited') return true;
|
|
506
|
+
return config.features.includes(feature);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Get limit value, handling -1 as Infinity
|
|
511
|
+
*/
|
|
512
|
+
export function getEffectiveLimit(limit: number): number {
|
|
513
|
+
return limit === -1 ? Infinity : limit;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Format limit for display
|
|
518
|
+
*/
|
|
519
|
+
export function formatLimit(limit: number): string {
|
|
520
|
+
return limit === -1 ? 'Unlimited' : limit.toLocaleString();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Get Stripe price ID for a tier (from environment)
|
|
525
|
+
*/
|
|
526
|
+
export function getStripePriceId(tier: PurchasableTier, interval: 'month' | 'year'): string | undefined {
|
|
527
|
+
const envKey = interval === 'year'
|
|
528
|
+
? `STRIPE_PRICE_${tier.toUpperCase()}_ANNUAL`
|
|
529
|
+
: `STRIPE_PRICE_${tier.toUpperCase()}_MONTHLY`;
|
|
530
|
+
|
|
531
|
+
return process.env[envKey] || process.env[`STRIPE_PRICE_ID_${tier.toUpperCase()}`];
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Map Stripe price ID back to tier
|
|
536
|
+
*/
|
|
537
|
+
export function getTierFromStripePriceId(priceId: string): Tier {
|
|
538
|
+
for (const tier of PURCHASABLE_TIERS) {
|
|
539
|
+
const monthlyId = getStripePriceId(tier, 'month');
|
|
540
|
+
const annualId = getStripePriceId(tier, 'year');
|
|
541
|
+
if (priceId === monthlyId || priceId === annualId) {
|
|
542
|
+
return tier;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return 'free';
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ============================================================================
|
|
549
|
+
// PRICING PAGE HELPERS
|
|
550
|
+
// ============================================================================
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Get pricing tiers formatted for landing page display
|
|
554
|
+
*/
|
|
555
|
+
export function getPricingPageTiers(): Array<{
|
|
556
|
+
id: Tier;
|
|
557
|
+
name: string;
|
|
558
|
+
price: number;
|
|
559
|
+
annual: number;
|
|
560
|
+
description: string;
|
|
561
|
+
popular: boolean;
|
|
562
|
+
features: string[];
|
|
563
|
+
}> {
|
|
564
|
+
return [
|
|
565
|
+
{
|
|
566
|
+
id: 'free',
|
|
567
|
+
name: TIER_CONFIG.free.name,
|
|
568
|
+
price: TIER_CONFIG.free.price,
|
|
569
|
+
annual: TIER_CONFIG.free.annualPrice,
|
|
570
|
+
description: TIER_CONFIG.free.description,
|
|
571
|
+
popular: false,
|
|
572
|
+
features: [
|
|
573
|
+
'Static code analysis',
|
|
574
|
+
'AI code validation',
|
|
575
|
+
'Ship badge generator',
|
|
576
|
+
`${formatLimit(TIER_CONFIG.free.limits.scansPerMonth)} scans/month`,
|
|
577
|
+
],
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
id: 'starter',
|
|
581
|
+
name: TIER_CONFIG.starter.name,
|
|
582
|
+
price: TIER_CONFIG.starter.price,
|
|
583
|
+
annual: TIER_CONFIG.starter.annualPrice,
|
|
584
|
+
description: TIER_CONFIG.starter.description,
|
|
585
|
+
popular: false,
|
|
586
|
+
features: [
|
|
587
|
+
'Everything in Free, plus:',
|
|
588
|
+
'Reality Mode browser testing',
|
|
589
|
+
'CI/CD deploy blocking',
|
|
590
|
+
'Mock detection',
|
|
591
|
+
`${formatLimit(TIER_CONFIG.starter.limits.scansPerMonth)} scans, ${formatLimit(TIER_CONFIG.starter.limits.realityRunsPerMonth)} Reality runs`,
|
|
592
|
+
],
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
id: 'pro',
|
|
596
|
+
name: TIER_CONFIG.pro.name,
|
|
597
|
+
price: TIER_CONFIG.pro.price,
|
|
598
|
+
annual: TIER_CONFIG.pro.annualPrice,
|
|
599
|
+
description: TIER_CONFIG.pro.description,
|
|
600
|
+
popular: true,
|
|
601
|
+
features: [
|
|
602
|
+
'Everything in Starter, plus:',
|
|
603
|
+
'AI Agent autonomous testing',
|
|
604
|
+
'Auto-fix with generated prompts',
|
|
605
|
+
'Autopilot continuous protection',
|
|
606
|
+
'MCP plugin for your IDE',
|
|
607
|
+
`${formatLimit(TIER_CONFIG.pro.limits.realityRunsPerMonth)} Reality, ${formatLimit(TIER_CONFIG.pro.limits.aiAgentRunsPerMonth)} AI Agent runs`,
|
|
608
|
+
],
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
id: 'compliance',
|
|
612
|
+
name: TIER_CONFIG.compliance.name,
|
|
613
|
+
price: TIER_CONFIG.compliance.price,
|
|
614
|
+
annual: TIER_CONFIG.compliance.annualPrice,
|
|
615
|
+
description: TIER_CONFIG.compliance.description,
|
|
616
|
+
popular: false,
|
|
617
|
+
features: [
|
|
618
|
+
'Everything in Pro, plus:',
|
|
619
|
+
'SOC2, HIPAA, GDPR, PCI-DSS',
|
|
620
|
+
'NIST and ISO 27001 frameworks',
|
|
621
|
+
'Audit-ready PDF reports',
|
|
622
|
+
`${formatLimit(TIER_CONFIG.compliance.limits.realityRunsPerMonth)} Reality, ${formatLimit(TIER_CONFIG.compliance.limits.aiAgentRunsPerMonth)} AI Agent runs`,
|
|
623
|
+
],
|
|
624
|
+
},
|
|
625
|
+
];
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ============================================================================
|
|
629
|
+
// RATE LIMIT HELPERS
|
|
630
|
+
// ============================================================================
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Get rate limit config for a tier (for rate-limiter middleware)
|
|
634
|
+
*/
|
|
635
|
+
export function getRateLimitForTier(tier: Tier): RateLimitConfig {
|
|
636
|
+
return TIER_CONFIG[tier].rateLimit;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Get user tiers formatted for rate limiter
|
|
641
|
+
*/
|
|
642
|
+
export function getRateLimiterTiers(): Record<Tier, {
|
|
643
|
+
name: string;
|
|
644
|
+
baseLimit: number;
|
|
645
|
+
burstLimit: number;
|
|
646
|
+
windowMs: number;
|
|
647
|
+
}> {
|
|
648
|
+
const result: Record<string, any> = {};
|
|
649
|
+
for (const tier of TIERS) {
|
|
650
|
+
const config = TIER_CONFIG[tier];
|
|
651
|
+
result[tier] = {
|
|
652
|
+
name: config.name,
|
|
653
|
+
baseLimit: config.rateLimit.requestsPerMinute,
|
|
654
|
+
burstLimit: config.rateLimit.burstLimit,
|
|
655
|
+
windowMs: config.rateLimit.windowMs,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
return result as Record<Tier, any>;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ============================================================================
|
|
662
|
+
// SEAT MANAGEMENT HELPERS
|
|
663
|
+
// ============================================================================
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Get seat pricing for a tier
|
|
667
|
+
*/
|
|
668
|
+
export function getSeatPricing(tier: Tier): SeatPricing {
|
|
669
|
+
return SEAT_PRICING[tier];
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Calculate effective team seats (base + purchased extras)
|
|
674
|
+
*/
|
|
675
|
+
export function calculateEffectiveSeats(tier: Tier, purchasedExtraSeats: number): number {
|
|
676
|
+
const baseSeats = TIER_CONFIG[tier].limits.teamMembers;
|
|
677
|
+
const seatConfig = SEAT_PRICING[tier];
|
|
678
|
+
|
|
679
|
+
// If tier doesn't support additional seats, return base only
|
|
680
|
+
if (!seatConfig.supportsAdditionalSeats) {
|
|
681
|
+
return baseSeats === -1 ? Infinity : baseSeats;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Handle unlimited base seats
|
|
685
|
+
if (baseSeats === -1) {
|
|
686
|
+
return Infinity;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Cap purchased seats at max allowed (if not unlimited)
|
|
690
|
+
let effectiveExtras = purchasedExtraSeats;
|
|
691
|
+
if (seatConfig.maxAdditionalSeats !== -1) {
|
|
692
|
+
effectiveExtras = Math.min(purchasedExtraSeats, seatConfig.maxAdditionalSeats);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return baseSeats + effectiveExtras;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Check if a member can be added given current seats and effective limit
|
|
700
|
+
*/
|
|
701
|
+
export function canAddMember(
|
|
702
|
+
tier: Tier,
|
|
703
|
+
currentMemberCount: number,
|
|
704
|
+
purchasedExtraSeats: number
|
|
705
|
+
): { allowed: boolean; reason?: string; effectiveSeats: number } {
|
|
706
|
+
const effectiveSeats = calculateEffectiveSeats(tier, purchasedExtraSeats);
|
|
707
|
+
|
|
708
|
+
if (effectiveSeats === Infinity) {
|
|
709
|
+
return { allowed: true, effectiveSeats };
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (currentMemberCount >= effectiveSeats) {
|
|
713
|
+
const seatConfig = SEAT_PRICING[tier];
|
|
714
|
+
const canPurchaseMore = seatConfig.supportsAdditionalSeats &&
|
|
715
|
+
(seatConfig.maxAdditionalSeats === -1 || purchasedExtraSeats < seatConfig.maxAdditionalSeats);
|
|
716
|
+
|
|
717
|
+
return {
|
|
718
|
+
allowed: false,
|
|
719
|
+
reason: canPurchaseMore
|
|
720
|
+
? `Seat limit reached (${currentMemberCount}/${effectiveSeats}). Purchase additional seats at $${seatConfig.monthlyPricePerSeat}/seat/month.`
|
|
721
|
+
: `Seat limit reached (${currentMemberCount}/${effectiveSeats}). Upgrade to a higher tier for more seats.`,
|
|
722
|
+
effectiveSeats,
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return { allowed: true, effectiveSeats };
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Calculate cost for additional seats
|
|
731
|
+
*/
|
|
732
|
+
export function calculateSeatCost(
|
|
733
|
+
tier: Tier,
|
|
734
|
+
additionalSeats: number,
|
|
735
|
+
billingInterval: 'month' | 'year'
|
|
736
|
+
): { total: number; perSeat: number; supported: boolean } {
|
|
737
|
+
const seatConfig = SEAT_PRICING[tier];
|
|
738
|
+
|
|
739
|
+
if (!seatConfig.supportsAdditionalSeats) {
|
|
740
|
+
return { total: 0, perSeat: 0, supported: false };
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const perSeat = billingInterval === 'year'
|
|
744
|
+
? seatConfig.annualPricePerSeat
|
|
745
|
+
: seatConfig.monthlyPricePerSeat;
|
|
746
|
+
|
|
747
|
+
return {
|
|
748
|
+
total: perSeat * additionalSeats,
|
|
749
|
+
perSeat,
|
|
750
|
+
supported: true,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Get Stripe seat price ID for a tier
|
|
756
|
+
*/
|
|
757
|
+
export function getStripeSeatPriceId(tier: Tier, interval: 'month' | 'year'): string | undefined {
|
|
758
|
+
const envKey = interval === 'year'
|
|
759
|
+
? `STRIPE_SEAT_PRICE_${tier.toUpperCase()}_ANNUAL`
|
|
760
|
+
: `STRIPE_SEAT_PRICE_${tier.toUpperCase()}_MONTHLY`;
|
|
761
|
+
|
|
762
|
+
return process.env[envKey];
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Format seat info for display
|
|
767
|
+
*/
|
|
768
|
+
export function formatSeatInfo(tier: Tier): string {
|
|
769
|
+
const config = TIER_CONFIG[tier];
|
|
770
|
+
const seatConfig = SEAT_PRICING[tier];
|
|
771
|
+
const baseSeats = config.limits.teamMembers;
|
|
772
|
+
|
|
773
|
+
if (baseSeats === -1) {
|
|
774
|
+
return 'Unlimited team members';
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (!seatConfig.supportsAdditionalSeats) {
|
|
778
|
+
return `${baseSeats} team member${baseSeats !== 1 ? 's' : ''}`;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
return `${baseSeats} seats included, +$${seatConfig.monthlyPricePerSeat}/seat/mo`;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Validate seat reduction (graceful handling)
|
|
786
|
+
* Returns info about whether reduction is safe or requires admin action
|
|
787
|
+
*/
|
|
788
|
+
export function validateSeatReduction(
|
|
789
|
+
currentMemberCount: number,
|
|
790
|
+
_currentEffectiveSeats: number, // Kept for API compatibility, may be used for logging
|
|
791
|
+
newEffectiveSeats: number
|
|
792
|
+
): {
|
|
793
|
+
safe: boolean;
|
|
794
|
+
requiresAction: boolean;
|
|
795
|
+
excessMembers: number;
|
|
796
|
+
message: string;
|
|
797
|
+
} {
|
|
798
|
+
if (newEffectiveSeats >= currentMemberCount) {
|
|
799
|
+
return {
|
|
800
|
+
safe: true,
|
|
801
|
+
requiresAction: false,
|
|
802
|
+
excessMembers: 0,
|
|
803
|
+
message: 'Seat reduction is safe.',
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const excessMembers = currentMemberCount - newEffectiveSeats;
|
|
808
|
+
|
|
809
|
+
return {
|
|
810
|
+
safe: false,
|
|
811
|
+
requiresAction: true,
|
|
812
|
+
excessMembers,
|
|
813
|
+
message: `Cannot reduce seats: ${excessMembers} member(s) would exceed the new limit. Remove members before reducing seats.`,
|
|
814
|
+
};
|
|
815
|
+
}
|