guardrail-core 1.0.0 → 2.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-enterprise.test.d.ts +7 -0
- package/dist/__tests__/autopilot-enterprise.test.d.ts.map +1 -0
- package/dist/__tests__/autopilot-enterprise.test.js +334 -0
- package/dist/autopilot/autopilot-runner.d.ts +9 -0
- package/dist/autopilot/autopilot-runner.d.ts.map +1 -1
- package/dist/autopilot/autopilot-runner.js +182 -1
- package/dist/autopilot/types.d.ts +18 -2
- package/dist/autopilot/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/smells/index.d.ts +59 -0
- package/dist/smells/index.d.ts.map +1 -0
- package/dist/smells/index.js +251 -0
- package/package.json +19 -2
- package/src/__tests__/autopilot.test.ts +0 -196
- package/src/__tests__/tier-config.test.ts +0 -289
- package/src/__tests__/utils/hash-inline.test.ts +0 -76
- package/src/__tests__/utils/hash.test.ts +0 -119
- package/src/__tests__/utils/simple.test.ts +0 -10
- package/src/__tests__/utils/utils-simple.test.ts +0 -5
- package/src/__tests__/utils/utils.test.ts +0 -203
- package/src/autopilot/autopilot-runner.ts +0 -503
- package/src/autopilot/index.ts +0 -6
- package/src/autopilot/types.ts +0 -119
- package/src/cache/index.ts +0 -7
- package/src/cache/redis-cache.d.ts +0 -155
- package/src/cache/redis-cache.d.ts.map +0 -1
- package/src/cache/redis-cache.ts +0 -517
- package/src/ci/github-actions.ts +0 -335
- package/src/ci/index.ts +0 -12
- package/src/ci/pre-commit.ts +0 -338
- package/src/db/usage-schema.prisma +0 -114
- package/src/entitlements.ts +0 -570
- package/src/env.d.ts +0 -68
- package/src/env.d.ts.map +0 -1
- package/src/env.ts +0 -247
- package/src/fix-packs/__tests__/generate-fix-packs.test.ts +0 -317
- package/src/fix-packs/generate-fix-packs.ts +0 -577
- package/src/fix-packs/index.ts +0 -8
- package/src/fix-packs/types.ts +0 -206
- package/src/index.d.ts +0 -7
- package/src/index.d.ts.map +0 -1
- package/src/index.ts +0 -12
- package/src/metrics/prometheus.d.ts +0 -104
- package/src/metrics/prometheus.d.ts.map +0 -1
- package/src/metrics/prometheus.ts +0 -446
- package/src/quota-ledger.ts +0 -548
- package/src/rbac/__tests__/permissions.test.ts +0 -446
- package/src/rbac/index.ts +0 -46
- package/src/rbac/permissions.ts +0 -301
- package/src/rbac/types.ts +0 -298
- package/src/tier-config.json +0 -157
- package/src/tier-config.ts +0 -815
- package/src/types.d.ts +0 -365
- package/src/types.d.ts.map +0 -1
- package/src/types.ts +0 -441
- package/src/utils.d.ts +0 -36
- package/src/utils.d.ts.map +0 -1
- package/src/utils.ts +0 -140
- package/src/verified-autofix/__tests__/format-validator.test.ts +0 -335
- package/src/verified-autofix/__tests__/pipeline.test.ts +0 -419
- package/src/verified-autofix/__tests__/repo-fingerprint.test.ts +0 -241
- package/src/verified-autofix/__tests__/workspace.test.ts +0 -373
- package/src/verified-autofix/format-validator.ts +0 -517
- package/src/verified-autofix/index.ts +0 -63
- package/src/verified-autofix/pipeline.ts +0 -403
- package/src/verified-autofix/repo-fingerprint.ts +0 -459
- package/src/verified-autofix/workspace.ts +0 -531
- package/src/verified-autofix.ts +0 -1187
- package/src/visualization/dependency-graph.d.ts +0 -85
- package/src/visualization/dependency-graph.d.ts.map +0 -1
- package/src/visualization/dependency-graph.ts +0 -495
- package/src/visualization/index.ts +0 -5
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
// Usage Tracking Schema for Guardrail Quotas
|
|
2
|
-
//
|
|
3
|
-
// INTEGRATION INSTRUCTIONS:
|
|
4
|
-
// Copy these model definitions to your main schema.prisma file.
|
|
5
|
-
// Ensure you have Organization and User models defined.
|
|
6
|
-
// Run: npx prisma migrate dev --name add-usage-tracking
|
|
7
|
-
//
|
|
8
|
-
// This file is a REFERENCE only - not directly used by Prisma.
|
|
9
|
-
|
|
10
|
-
// Usage Record - tracks individual usage events with idempotency
|
|
11
|
-
// model UsageRecord {
|
|
12
|
-
// id String @id @default(cuid())
|
|
13
|
-
// orgId String
|
|
14
|
-
// userId String?
|
|
15
|
-
// type String // 'scan' | 'reality' | 'agent' | 'fix' | 'gate'
|
|
16
|
-
// count Int @default(1)
|
|
17
|
-
// requestId String? @unique // For idempotency
|
|
18
|
-
// timestamp DateTime @default(now())
|
|
19
|
-
// metadata Json? // Additional context (project, command, etc.)
|
|
20
|
-
// synced Boolean @default(true) // For offline sync tracking
|
|
21
|
-
//
|
|
22
|
-
// org Organization @relation(fields: [orgId], references: [id])
|
|
23
|
-
// user User? @relation(fields: [userId], references: [id])
|
|
24
|
-
//
|
|
25
|
-
// @@index([orgId, type, timestamp])
|
|
26
|
-
// @@index([requestId])
|
|
27
|
-
// @@index([timestamp])
|
|
28
|
-
// }
|
|
29
|
-
|
|
30
|
-
model UsageRecordStandalone {
|
|
31
|
-
id String @id @default(cuid())
|
|
32
|
-
orgId String
|
|
33
|
-
userId String?
|
|
34
|
-
type String
|
|
35
|
-
count Int @default(1)
|
|
36
|
-
requestId String? @unique
|
|
37
|
-
timestamp DateTime @default(now())
|
|
38
|
-
metadata Json?
|
|
39
|
-
synced Boolean @default(true)
|
|
40
|
-
|
|
41
|
-
@@index([orgId, type, timestamp])
|
|
42
|
-
@@index([requestId])
|
|
43
|
-
@@index([timestamp])
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Usage Summary - aggregated usage per billing period
|
|
47
|
-
model UsageSummaryStandalone {
|
|
48
|
-
id String @id @default(cuid())
|
|
49
|
-
orgId String
|
|
50
|
-
periodStart DateTime
|
|
51
|
-
periodEnd DateTime
|
|
52
|
-
scans Int @default(0)
|
|
53
|
-
realityRuns Int @default(0)
|
|
54
|
-
agentRuns Int @default(0)
|
|
55
|
-
fixRuns Int @default(0)
|
|
56
|
-
gateRuns Int @default(0)
|
|
57
|
-
lastUpdated DateTime @updatedAt
|
|
58
|
-
|
|
59
|
-
@@unique([orgId, periodStart])
|
|
60
|
-
@@index([orgId])
|
|
61
|
-
@@index([periodEnd])
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Subscription - tracks tier and billing info
|
|
65
|
-
model SubscriptionStandalone {
|
|
66
|
-
id String @id @default(cuid())
|
|
67
|
-
orgId String @unique
|
|
68
|
-
tier String @default("free")
|
|
69
|
-
status String @default("active")
|
|
70
|
-
stripeCustomerId String?
|
|
71
|
-
stripeSubscriptionId String?
|
|
72
|
-
stripePriceId String?
|
|
73
|
-
currentPeriodStart DateTime?
|
|
74
|
-
currentPeriodEnd DateTime?
|
|
75
|
-
cancelAtPeriodEnd Boolean @default(false)
|
|
76
|
-
purchasedSeats Int @default(0)
|
|
77
|
-
createdAt DateTime @default(now())
|
|
78
|
-
updatedAt DateTime @updatedAt
|
|
79
|
-
|
|
80
|
-
@@index([stripeCustomerId])
|
|
81
|
-
@@index([stripeSubscriptionId])
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// API Key - for CLI authentication
|
|
85
|
-
model ApiKeyStandalone {
|
|
86
|
-
id String @id @default(cuid())
|
|
87
|
-
key String @unique
|
|
88
|
-
keyPrefix String
|
|
89
|
-
name String?
|
|
90
|
-
orgId String
|
|
91
|
-
userId String
|
|
92
|
-
scopes String @default("scan,ship,reality,agent") // Comma-separated list
|
|
93
|
-
lastUsedAt DateTime?
|
|
94
|
-
expiresAt DateTime?
|
|
95
|
-
revokedAt DateTime?
|
|
96
|
-
createdAt DateTime @default(now())
|
|
97
|
-
|
|
98
|
-
@@index([orgId])
|
|
99
|
-
@@index([userId])
|
|
100
|
-
@@index([keyPrefix])
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Rate Limit Record - for API rate limiting
|
|
104
|
-
model RateLimitRecord {
|
|
105
|
-
id String @id @default(cuid())
|
|
106
|
-
identifier String // API key hash or IP
|
|
107
|
-
endpoint String
|
|
108
|
-
count Int @default(1)
|
|
109
|
-
windowStart DateTime
|
|
110
|
-
windowEnd DateTime
|
|
111
|
-
|
|
112
|
-
@@unique([identifier, endpoint, windowStart])
|
|
113
|
-
@@index([windowEnd])
|
|
114
|
-
}
|
package/src/entitlements.ts
DELETED
|
@@ -1,570 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Entitlements System - SINGLE SOURCE OF TRUTH
|
|
3
|
-
*
|
|
4
|
-
* This module is the canonical entitlements implementation for Guardrail.
|
|
5
|
-
* It handles feature access, usage limits, tier enforcement, and seat management.
|
|
6
|
-
*
|
|
7
|
-
* IMPORTANT: This TypeScript file is compiled to dist/entitlements.js
|
|
8
|
-
* DO NOT create separate entitlements.js files elsewhere in the codebase.
|
|
9
|
-
* All consumers (API, CLI, etc.) should import from @guardrail/core.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import * as fs from 'fs';
|
|
13
|
-
import * as os from 'os';
|
|
14
|
-
import * as path from 'path';
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
Feature,
|
|
18
|
-
SEAT_PRICING,
|
|
19
|
-
SeatPricing,
|
|
20
|
-
TIER_CONFIG,
|
|
21
|
-
Tier,
|
|
22
|
-
TierConfig,
|
|
23
|
-
calculateEffectiveSeats,
|
|
24
|
-
canAddMember,
|
|
25
|
-
formatSeatInfo,
|
|
26
|
-
getMinimumTierForFeature,
|
|
27
|
-
getTierConfig,
|
|
28
|
-
isValidTier,
|
|
29
|
-
validateSeatReduction,
|
|
30
|
-
} from './tier-config';
|
|
31
|
-
|
|
32
|
-
// Re-export types for consumers
|
|
33
|
-
export type { Feature, SeatPricing, Tier, TierConfig };
|
|
34
|
-
|
|
35
|
-
// Re-export values for consumers
|
|
36
|
-
export {
|
|
37
|
-
SEAT_PRICING,
|
|
38
|
-
TIER_CONFIG,
|
|
39
|
-
calculateEffectiveSeats,
|
|
40
|
-
canAddMember,
|
|
41
|
-
formatSeatInfo,
|
|
42
|
-
getMinimumTierForFeature,
|
|
43
|
-
getTierConfig,
|
|
44
|
-
isValidTier,
|
|
45
|
-
validateSeatReduction
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// ============================================================================
|
|
49
|
-
// TYPES
|
|
50
|
-
// ============================================================================
|
|
51
|
-
|
|
52
|
-
export interface UsageRecord {
|
|
53
|
-
tier: Tier;
|
|
54
|
-
userId?: string;
|
|
55
|
-
email?: string;
|
|
56
|
-
periodStart: string;
|
|
57
|
-
periodEnd: string;
|
|
58
|
-
usage: {
|
|
59
|
-
scans: number;
|
|
60
|
-
realityRuns: number;
|
|
61
|
-
aiAgentRuns: number;
|
|
62
|
-
gateRuns: number;
|
|
63
|
-
fixRuns: number;
|
|
64
|
-
};
|
|
65
|
-
lastUpdated: string;
|
|
66
|
-
lastServerSync?: string;
|
|
67
|
-
pendingSync?: boolean;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface EntitlementCheck {
|
|
71
|
-
allowed: boolean;
|
|
72
|
-
reason?: string;
|
|
73
|
-
usage?: number;
|
|
74
|
-
limit?: number;
|
|
75
|
-
upgradePrompt?: string;
|
|
76
|
-
source?: 'server' | 'cache' | 'local' | 'offline';
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface SeatCheck {
|
|
80
|
-
allowed: boolean;
|
|
81
|
-
reason?: string;
|
|
82
|
-
effectiveSeats: number;
|
|
83
|
-
baseSeats: number;
|
|
84
|
-
purchasedSeats: number;
|
|
85
|
-
currentMembers: number;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export interface OrganizationSeats {
|
|
89
|
-
tier: Tier;
|
|
90
|
-
baseSeats: number;
|
|
91
|
-
purchasedExtraSeats: number;
|
|
92
|
-
effectiveSeats: number;
|
|
93
|
-
currentMembers: number;
|
|
94
|
-
seatPricing: SeatPricing;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// ============================================================================
|
|
98
|
-
// ENTITLEMENTS MANAGER
|
|
99
|
-
// ============================================================================
|
|
100
|
-
|
|
101
|
-
export class EntitlementsManager {
|
|
102
|
-
private configDir: string;
|
|
103
|
-
private usageFile: string;
|
|
104
|
-
private licenseFile: string;
|
|
105
|
-
|
|
106
|
-
constructor() {
|
|
107
|
-
this.configDir = path.join(os.homedir(), '.guardrail');
|
|
108
|
-
this.usageFile = path.join(this.configDir, 'usage.json');
|
|
109
|
-
this.licenseFile = path.join(this.configDir, 'license.json');
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Get current tier from license file or environment
|
|
114
|
-
*/
|
|
115
|
-
async getCurrentTier(): Promise<Tier> {
|
|
116
|
-
// Skip entitlements check if explicitly disabled
|
|
117
|
-
if (process.env['GUARDRAIL_SKIP_ENTITLEMENTS'] === '1') {
|
|
118
|
-
return 'unlimited';
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Check environment override (for CI/testing)
|
|
122
|
-
if (process.env['GUARDRAIL_TIER']) {
|
|
123
|
-
return process.env['GUARDRAIL_TIER'] as Tier;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Check for license file
|
|
127
|
-
try {
|
|
128
|
-
const license = await this.readLicense();
|
|
129
|
-
if (license?.tier && isValidTier(license.tier)) {
|
|
130
|
-
// Check expiration
|
|
131
|
-
if (license.expiresAt && new Date(license.expiresAt) < new Date()) {
|
|
132
|
-
return 'free';
|
|
133
|
-
}
|
|
134
|
-
return license.tier;
|
|
135
|
-
}
|
|
136
|
-
} catch {
|
|
137
|
-
// No license file
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Check for API key - validate against server (NO local tier parsing)
|
|
141
|
-
const apiKey = process.env['GUARDRAIL_API_KEY'];
|
|
142
|
-
if (apiKey) {
|
|
143
|
-
const tier = await this.validateApiKeyWithServer(apiKey);
|
|
144
|
-
if (tier) return tier;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return 'free';
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Validate API key against server and return tier
|
|
152
|
-
*
|
|
153
|
-
* SECURITY: Tier is determined server-side only.
|
|
154
|
-
* The API key string contains NO tier information.
|
|
155
|
-
*/
|
|
156
|
-
private async validateApiKeyWithServer(apiKey: string): Promise<Tier | null> {
|
|
157
|
-
const apiUrl = process.env['GUARDRAIL_API_URL'] || 'https://api.getguardrail.io';
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
const response = await fetch(`${apiUrl}/api/api-keys/validate`, {
|
|
161
|
-
method: 'POST',
|
|
162
|
-
headers: {
|
|
163
|
-
'Content-Type': 'application/json',
|
|
164
|
-
},
|
|
165
|
-
body: JSON.stringify({ apiKey }),
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
if (!response.ok) {
|
|
169
|
-
return null;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const result = await response.json() as { valid: boolean; tier?: string };
|
|
173
|
-
|
|
174
|
-
if (result.valid && result.tier && isValidTier(result.tier)) {
|
|
175
|
-
return result.tier as Tier;
|
|
176
|
-
}
|
|
177
|
-
} catch {
|
|
178
|
-
// Network error or server unavailable - fall back to free tier
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Check if a feature is available for the current tier
|
|
186
|
-
*/
|
|
187
|
-
async checkFeature(feature: Feature): Promise<EntitlementCheck> {
|
|
188
|
-
const tier = await this.getCurrentTier();
|
|
189
|
-
const config = TIER_CONFIG[tier];
|
|
190
|
-
|
|
191
|
-
// Unlimited tier has all features
|
|
192
|
-
if (tier === 'unlimited' || config.features.includes(feature)) {
|
|
193
|
-
return { allowed: true };
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Find the minimum tier that has this feature
|
|
197
|
-
const requiredTier = getMinimumTierForFeature(feature);
|
|
198
|
-
|
|
199
|
-
return {
|
|
200
|
-
allowed: false,
|
|
201
|
-
reason: `'${feature}' requires ${requiredTier || 'higher'} tier`,
|
|
202
|
-
upgradePrompt: this.formatUpgradePrompt(tier, requiredTier, feature),
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Check usage limits
|
|
208
|
-
*/
|
|
209
|
-
async checkLimit(limitType: 'scans' | 'realityRuns' | 'aiAgentRuns'): Promise<EntitlementCheck> {
|
|
210
|
-
const tier = await this.getCurrentTier();
|
|
211
|
-
const config = TIER_CONFIG[tier];
|
|
212
|
-
const usage = await this.getUsage();
|
|
213
|
-
|
|
214
|
-
const limitMap: Record<string, keyof TierConfig['limits']> = {
|
|
215
|
-
scans: 'scansPerMonth',
|
|
216
|
-
realityRuns: 'realityRunsPerMonth',
|
|
217
|
-
aiAgentRuns: 'aiAgentRunsPerMonth',
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
const limitKey = limitMap[limitType] as keyof TierConfig['limits'];
|
|
221
|
-
const limit = config.limits[limitKey] as number;
|
|
222
|
-
const current = usage.usage[limitType] || 0;
|
|
223
|
-
|
|
224
|
-
// Handle unlimited (-1)
|
|
225
|
-
if (limit === -1 || current < limit) {
|
|
226
|
-
return {
|
|
227
|
-
allowed: true,
|
|
228
|
-
usage: current,
|
|
229
|
-
limit: limit === -1 ? -1 : limit,
|
|
230
|
-
source: 'local',
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return {
|
|
235
|
-
allowed: false,
|
|
236
|
-
reason: `Monthly ${limitType} limit reached (${current}/${limit})`,
|
|
237
|
-
usage: current,
|
|
238
|
-
limit,
|
|
239
|
-
upgradePrompt: this.formatLimitUpgradePrompt(tier, limitType, current, limit),
|
|
240
|
-
source: 'local',
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Track usage
|
|
246
|
-
*/
|
|
247
|
-
async trackUsage(type: 'scans' | 'realityRuns' | 'aiAgentRuns' | 'gateRuns' | 'fixRuns', count: number = 1): Promise<void> {
|
|
248
|
-
const usage = await this.getUsage();
|
|
249
|
-
usage.usage[type] = (usage.usage[type] || 0) + count;
|
|
250
|
-
usage.lastUpdated = new Date().toISOString();
|
|
251
|
-
await this.saveUsage(usage);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Enforce feature access (throws if not allowed)
|
|
256
|
-
*/
|
|
257
|
-
async enforceFeature(feature: Feature): Promise<void> {
|
|
258
|
-
const check = await this.checkFeature(feature);
|
|
259
|
-
if (!check.allowed) {
|
|
260
|
-
const error = new Error(check.reason) as any;
|
|
261
|
-
error.code = 'FEATURE_NOT_AVAILABLE';
|
|
262
|
-
error.upgradePrompt = check.upgradePrompt;
|
|
263
|
-
error.feature = feature;
|
|
264
|
-
throw error;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Enforce usage limits (throws if exceeded)
|
|
270
|
-
*/
|
|
271
|
-
async enforceLimit(limitType: 'scans' | 'realityRuns' | 'aiAgentRuns'): Promise<void> {
|
|
272
|
-
const check = await this.checkLimit(limitType);
|
|
273
|
-
if (!check.allowed) {
|
|
274
|
-
const error = new Error(check.reason) as any;
|
|
275
|
-
error.code = 'LIMIT_EXCEEDED';
|
|
276
|
-
error.upgradePrompt = check.upgradePrompt;
|
|
277
|
-
error.usage = check.usage;
|
|
278
|
-
error.limit = check.limit;
|
|
279
|
-
throw error;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// ============================================================================
|
|
284
|
-
// SEAT MANAGEMENT
|
|
285
|
-
// ============================================================================
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Check if a member can be added to an organization
|
|
289
|
-
*/
|
|
290
|
-
checkSeatLimit(
|
|
291
|
-
tier: Tier,
|
|
292
|
-
currentMemberCount: number,
|
|
293
|
-
purchasedExtraSeats: number = 0
|
|
294
|
-
): SeatCheck {
|
|
295
|
-
const config = TIER_CONFIG[tier];
|
|
296
|
-
const baseSeats = config.limits.teamMembers;
|
|
297
|
-
const result = canAddMember(tier, currentMemberCount, purchasedExtraSeats);
|
|
298
|
-
|
|
299
|
-
return {
|
|
300
|
-
allowed: result.allowed,
|
|
301
|
-
reason: result.reason,
|
|
302
|
-
effectiveSeats: result.effectiveSeats === Infinity ? -1 : result.effectiveSeats,
|
|
303
|
-
baseSeats: baseSeats === -1 ? -1 : baseSeats,
|
|
304
|
-
purchasedSeats: purchasedExtraSeats,
|
|
305
|
-
currentMembers: currentMemberCount,
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Get organization seat information
|
|
311
|
-
*/
|
|
312
|
-
getOrganizationSeats(
|
|
313
|
-
tier: Tier,
|
|
314
|
-
purchasedExtraSeats: number,
|
|
315
|
-
currentMembers: number
|
|
316
|
-
): OrganizationSeats {
|
|
317
|
-
const config = TIER_CONFIG[tier];
|
|
318
|
-
const baseSeats = config.limits.teamMembers;
|
|
319
|
-
const effectiveSeats = calculateEffectiveSeats(tier, purchasedExtraSeats);
|
|
320
|
-
|
|
321
|
-
return {
|
|
322
|
-
tier,
|
|
323
|
-
baseSeats: baseSeats === -1 ? -1 : baseSeats,
|
|
324
|
-
purchasedExtraSeats,
|
|
325
|
-
effectiveSeats: effectiveSeats === Infinity ? -1 : effectiveSeats,
|
|
326
|
-
currentMembers,
|
|
327
|
-
seatPricing: SEAT_PRICING[tier],
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Validate seat reduction before processing
|
|
333
|
-
*/
|
|
334
|
-
validateSeatReduction(
|
|
335
|
-
currentMemberCount: number,
|
|
336
|
-
currentPurchasedSeats: number,
|
|
337
|
-
newPurchasedSeats: number,
|
|
338
|
-
tier: Tier
|
|
339
|
-
): { safe: boolean; requiresAction: boolean; excessMembers: number; message: string } {
|
|
340
|
-
const currentEffective = calculateEffectiveSeats(tier, currentPurchasedSeats);
|
|
341
|
-
const newEffective = calculateEffectiveSeats(tier, newPurchasedSeats);
|
|
342
|
-
|
|
343
|
-
return validateSeatReduction(
|
|
344
|
-
currentMemberCount,
|
|
345
|
-
currentEffective === Infinity ? -1 : currentEffective,
|
|
346
|
-
newEffective === Infinity ? -1 : newEffective
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// ============================================================================
|
|
351
|
-
// USAGE MANAGEMENT
|
|
352
|
-
// ============================================================================
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Get usage for current billing period
|
|
356
|
-
*/
|
|
357
|
-
async getUsage(): Promise<UsageRecord> {
|
|
358
|
-
try {
|
|
359
|
-
await this.ensureConfigDir();
|
|
360
|
-
const content = await fs.promises.readFile(this.usageFile, 'utf8');
|
|
361
|
-
const usage = JSON.parse(content) as UsageRecord;
|
|
362
|
-
|
|
363
|
-
// Check if we need to reset for new period
|
|
364
|
-
if (this.isNewBillingPeriod(usage.periodStart)) {
|
|
365
|
-
return this.createNewUsageRecord();
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
return usage;
|
|
369
|
-
} catch {
|
|
370
|
-
return this.createNewUsageRecord();
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Get tier configuration
|
|
376
|
-
*/
|
|
377
|
-
getTierConfig(tier: Tier): TierConfig {
|
|
378
|
-
return TIER_CONFIG[tier];
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* Get all tier configurations
|
|
383
|
-
*/
|
|
384
|
-
getAllTiers(): Record<Tier, TierConfig> {
|
|
385
|
-
return TIER_CONFIG;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Get usage summary for display
|
|
390
|
-
*/
|
|
391
|
-
async getUsageSummary(): Promise<string> {
|
|
392
|
-
const tier = await this.getCurrentTier();
|
|
393
|
-
const config = TIER_CONFIG[tier];
|
|
394
|
-
const usage = await this.getUsage();
|
|
395
|
-
|
|
396
|
-
const formatLimit = (current: number, limit: number): string => {
|
|
397
|
-
if (limit === -1) return `${current} (unlimited)`;
|
|
398
|
-
const pct = Math.round((current / limit) * 100);
|
|
399
|
-
const bar = this.progressBar(pct);
|
|
400
|
-
return `${current}/${limit} ${bar} ${pct}%`;
|
|
401
|
-
};
|
|
402
|
-
|
|
403
|
-
let summary = '\n';
|
|
404
|
-
summary += `📊 Usage Summary (${config.name} tier)\n`;
|
|
405
|
-
summary += '─'.repeat(50) + '\n';
|
|
406
|
-
summary += `Scans: ${formatLimit(usage.usage.scans, config.limits.scansPerMonth)}\n`;
|
|
407
|
-
summary += `Reality Runs: ${formatLimit(usage.usage.realityRuns, config.limits.realityRunsPerMonth)}\n`;
|
|
408
|
-
summary += `AI Agent: ${formatLimit(usage.usage.aiAgentRuns, config.limits.aiAgentRunsPerMonth)}\n`;
|
|
409
|
-
summary += `Team Seats: ${formatSeatInfo(tier)}\n`;
|
|
410
|
-
summary += '─'.repeat(50) + '\n';
|
|
411
|
-
summary += `Period: ${usage.periodStart.split('T')[0]} to ${usage.periodEnd.split('T')[0]}\n`;
|
|
412
|
-
|
|
413
|
-
return summary;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// ============================================================================
|
|
417
|
-
// UPGRADE PROMPTS
|
|
418
|
-
// ============================================================================
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* Format upgrade prompt for CLI output
|
|
422
|
-
*/
|
|
423
|
-
formatUpgradePrompt(currentTier: Tier, requiredTier: Tier | null, feature: Feature): string {
|
|
424
|
-
const required = requiredTier ? TIER_CONFIG[requiredTier] : null;
|
|
425
|
-
|
|
426
|
-
let prompt = '\n';
|
|
427
|
-
prompt += '╭─────────────────────────────────────────────────────────────╮\n';
|
|
428
|
-
prompt += '│ ⚡ UPGRADE REQUIRED │\n';
|
|
429
|
-
prompt += '├─────────────────────────────────────────────────────────────┤\n';
|
|
430
|
-
prompt += `│ Feature: ${feature.padEnd(48)}│\n`;
|
|
431
|
-
prompt += `│ Your tier: ${currentTier.padEnd(46)}│\n`;
|
|
432
|
-
|
|
433
|
-
if (required) {
|
|
434
|
-
prompt += `│ Required: ${requiredTier} ($${required.price}/month)`.padEnd(62) + '│\n';
|
|
435
|
-
prompt += '├─────────────────────────────────────────────────────────────┤\n';
|
|
436
|
-
prompt += `│ ${required.name} includes:`.padEnd(62) + '│\n';
|
|
437
|
-
|
|
438
|
-
// Show key features of required tier
|
|
439
|
-
const keyFeatures = required.features.slice(0, 5);
|
|
440
|
-
for (const f of keyFeatures) {
|
|
441
|
-
prompt += `│ ✓ ${f}`.padEnd(62) + '│\n';
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
prompt += '├─────────────────────────────────────────────────────────────┤\n';
|
|
446
|
-
prompt += '│ → guardrail upgrade │\n';
|
|
447
|
-
prompt += '│ → https://getguardrail.io/pricing │\n';
|
|
448
|
-
prompt += '╰─────────────────────────────────────────────────────────────╯\n';
|
|
449
|
-
|
|
450
|
-
return prompt;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* Format limit exceeded prompt
|
|
455
|
-
*/
|
|
456
|
-
formatLimitUpgradePrompt(currentTier: Tier, limitType: string, current: number, limit: number): string {
|
|
457
|
-
const config = TIER_CONFIG[currentTier];
|
|
458
|
-
const nextConfig = TIER_CONFIG[config.upsell.nextTier];
|
|
459
|
-
|
|
460
|
-
let prompt = '\n';
|
|
461
|
-
prompt += '╭─────────────────────────────────────────────────────────────╮\n';
|
|
462
|
-
prompt += '│ ⚠️ MONTHLY LIMIT REACHED │\n';
|
|
463
|
-
prompt += '├─────────────────────────────────────────────────────────────┤\n';
|
|
464
|
-
prompt += `│ ${limitType}: ${current}/${limit} used this month`.padEnd(62) + '│\n';
|
|
465
|
-
prompt += `│ Your tier: ${currentTier} ($${config.price}/month)`.padEnd(62) + '│\n';
|
|
466
|
-
prompt += '├─────────────────────────────────────────────────────────────┤\n';
|
|
467
|
-
prompt += `│ ${config.upsell.message}`.substring(0, 58).padEnd(62) + '│\n';
|
|
468
|
-
|
|
469
|
-
if (nextConfig && config.upsell.nextTier !== 'unlimited') {
|
|
470
|
-
const nextLimitMap: Record<string, keyof TierConfig['limits']> = {
|
|
471
|
-
scans: 'scansPerMonth',
|
|
472
|
-
realityRuns: 'realityRunsPerMonth',
|
|
473
|
-
aiAgentRuns: 'aiAgentRunsPerMonth',
|
|
474
|
-
};
|
|
475
|
-
const nextLimit = nextConfig.limits[nextLimitMap[limitType] || 'scansPerMonth'];
|
|
476
|
-
prompt += '├─────────────────────────────────────────────────────────────┤\n';
|
|
477
|
-
prompt += `│ ${nextConfig.name} ($${nextConfig.price}/mo): ${nextLimit === -1 ? 'Unlimited' : nextLimit} ${limitType}/month`.padEnd(62) + '│\n';
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
prompt += '├─────────────────────────────────────────────────────────────┤\n';
|
|
481
|
-
prompt += '│ → guardrail upgrade │\n';
|
|
482
|
-
prompt += '│ → https://getguardrail.io/pricing │\n';
|
|
483
|
-
prompt += '╰─────────────────────────────────────────────────────────────╯\n';
|
|
484
|
-
|
|
485
|
-
return prompt;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// ============================================================================
|
|
489
|
-
// PRIVATE HELPERS
|
|
490
|
-
// ============================================================================
|
|
491
|
-
|
|
492
|
-
private isNewBillingPeriod(periodStart: string): boolean {
|
|
493
|
-
const start = new Date(periodStart);
|
|
494
|
-
const now = new Date();
|
|
495
|
-
|
|
496
|
-
// Monthly billing period
|
|
497
|
-
const nextPeriod = new Date(start);
|
|
498
|
-
nextPeriod.setMonth(nextPeriod.getMonth() + 1);
|
|
499
|
-
|
|
500
|
-
return now >= nextPeriod;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
private createNewUsageRecord(): UsageRecord {
|
|
504
|
-
const now = new Date();
|
|
505
|
-
const periodEnd = new Date(now);
|
|
506
|
-
periodEnd.setMonth(periodEnd.getMonth() + 1);
|
|
507
|
-
|
|
508
|
-
return {
|
|
509
|
-
tier: 'free',
|
|
510
|
-
periodStart: now.toISOString(),
|
|
511
|
-
periodEnd: periodEnd.toISOString(),
|
|
512
|
-
usage: {
|
|
513
|
-
scans: 0,
|
|
514
|
-
realityRuns: 0,
|
|
515
|
-
aiAgentRuns: 0,
|
|
516
|
-
gateRuns: 0,
|
|
517
|
-
fixRuns: 0,
|
|
518
|
-
},
|
|
519
|
-
lastUpdated: now.toISOString(),
|
|
520
|
-
};
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
private async ensureConfigDir(): Promise<void> {
|
|
524
|
-
try {
|
|
525
|
-
await fs.promises.mkdir(this.configDir, { recursive: true });
|
|
526
|
-
} catch {
|
|
527
|
-
// Directory exists
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
private async saveUsage(usage: UsageRecord): Promise<void> {
|
|
532
|
-
await this.ensureConfigDir();
|
|
533
|
-
await fs.promises.writeFile(this.usageFile, JSON.stringify(usage, null, 2));
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
private async readLicense(): Promise<{ tier: Tier; expiresAt?: string; apiKey?: string } | null> {
|
|
537
|
-
try {
|
|
538
|
-
const content = await fs.promises.readFile(this.licenseFile, 'utf8');
|
|
539
|
-
return JSON.parse(content);
|
|
540
|
-
} catch {
|
|
541
|
-
return null;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
private progressBar(percent: number): string {
|
|
546
|
-
const filled = Math.min(10, Math.round(percent / 10));
|
|
547
|
-
const empty = 10 - filled;
|
|
548
|
-
const color = percent >= 90 ? '🔴' : percent >= 70 ? '🟡' : '🟢';
|
|
549
|
-
return `[${color.repeat(filled)}${'░'.repeat(empty)}]`;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// ============================================================================
|
|
554
|
-
// SINGLETON EXPORT
|
|
555
|
-
// ============================================================================
|
|
556
|
-
|
|
557
|
-
export const entitlements = new EntitlementsManager();
|
|
558
|
-
|
|
559
|
-
// Convenience exports
|
|
560
|
-
export const checkFeature = (feature: Feature) => entitlements.checkFeature(feature);
|
|
561
|
-
export const checkLimit = (limitType: 'scans' | 'realityRuns' | 'aiAgentRuns') => entitlements.checkLimit(limitType);
|
|
562
|
-
export const enforceFeature = (feature: Feature) => entitlements.enforceFeature(feature);
|
|
563
|
-
export const enforceLimit = (limitType: 'scans' | 'realityRuns' | 'aiAgentRuns') => entitlements.enforceLimit(limitType);
|
|
564
|
-
export const trackUsage = (type: 'scans' | 'realityRuns' | 'aiAgentRuns' | 'gateRuns' | 'fixRuns', count?: number) => entitlements.trackUsage(type, count);
|
|
565
|
-
export const getCurrentTier = () => entitlements.getCurrentTier();
|
|
566
|
-
export const getUsageSummary = () => entitlements.getUsageSummary();
|
|
567
|
-
export const checkSeatLimit = (tier: Tier, currentMemberCount: number, purchasedExtraSeats?: number) =>
|
|
568
|
-
entitlements.checkSeatLimit(tier, currentMemberCount, purchasedExtraSeats);
|
|
569
|
-
export const getOrganizationSeats = (tier: Tier, purchasedExtraSeats: number, currentMembers: number) =>
|
|
570
|
-
entitlements.getOrganizationSeats(tier, purchasedExtraSeats, currentMembers);
|