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.
Files changed (74) hide show
  1. package/dist/__tests__/autopilot-enterprise.test.d.ts +7 -0
  2. package/dist/__tests__/autopilot-enterprise.test.d.ts.map +1 -0
  3. package/dist/__tests__/autopilot-enterprise.test.js +334 -0
  4. package/dist/autopilot/autopilot-runner.d.ts +9 -0
  5. package/dist/autopilot/autopilot-runner.d.ts.map +1 -1
  6. package/dist/autopilot/autopilot-runner.js +182 -1
  7. package/dist/autopilot/types.d.ts +18 -2
  8. package/dist/autopilot/types.d.ts.map +1 -1
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -0
  12. package/dist/smells/index.d.ts +59 -0
  13. package/dist/smells/index.d.ts.map +1 -0
  14. package/dist/smells/index.js +251 -0
  15. package/package.json +19 -2
  16. package/src/__tests__/autopilot.test.ts +0 -196
  17. package/src/__tests__/tier-config.test.ts +0 -289
  18. package/src/__tests__/utils/hash-inline.test.ts +0 -76
  19. package/src/__tests__/utils/hash.test.ts +0 -119
  20. package/src/__tests__/utils/simple.test.ts +0 -10
  21. package/src/__tests__/utils/utils-simple.test.ts +0 -5
  22. package/src/__tests__/utils/utils.test.ts +0 -203
  23. package/src/autopilot/autopilot-runner.ts +0 -503
  24. package/src/autopilot/index.ts +0 -6
  25. package/src/autopilot/types.ts +0 -119
  26. package/src/cache/index.ts +0 -7
  27. package/src/cache/redis-cache.d.ts +0 -155
  28. package/src/cache/redis-cache.d.ts.map +0 -1
  29. package/src/cache/redis-cache.ts +0 -517
  30. package/src/ci/github-actions.ts +0 -335
  31. package/src/ci/index.ts +0 -12
  32. package/src/ci/pre-commit.ts +0 -338
  33. package/src/db/usage-schema.prisma +0 -114
  34. package/src/entitlements.ts +0 -570
  35. package/src/env.d.ts +0 -68
  36. package/src/env.d.ts.map +0 -1
  37. package/src/env.ts +0 -247
  38. package/src/fix-packs/__tests__/generate-fix-packs.test.ts +0 -317
  39. package/src/fix-packs/generate-fix-packs.ts +0 -577
  40. package/src/fix-packs/index.ts +0 -8
  41. package/src/fix-packs/types.ts +0 -206
  42. package/src/index.d.ts +0 -7
  43. package/src/index.d.ts.map +0 -1
  44. package/src/index.ts +0 -12
  45. package/src/metrics/prometheus.d.ts +0 -104
  46. package/src/metrics/prometheus.d.ts.map +0 -1
  47. package/src/metrics/prometheus.ts +0 -446
  48. package/src/quota-ledger.ts +0 -548
  49. package/src/rbac/__tests__/permissions.test.ts +0 -446
  50. package/src/rbac/index.ts +0 -46
  51. package/src/rbac/permissions.ts +0 -301
  52. package/src/rbac/types.ts +0 -298
  53. package/src/tier-config.json +0 -157
  54. package/src/tier-config.ts +0 -815
  55. package/src/types.d.ts +0 -365
  56. package/src/types.d.ts.map +0 -1
  57. package/src/types.ts +0 -441
  58. package/src/utils.d.ts +0 -36
  59. package/src/utils.d.ts.map +0 -1
  60. package/src/utils.ts +0 -140
  61. package/src/verified-autofix/__tests__/format-validator.test.ts +0 -335
  62. package/src/verified-autofix/__tests__/pipeline.test.ts +0 -419
  63. package/src/verified-autofix/__tests__/repo-fingerprint.test.ts +0 -241
  64. package/src/verified-autofix/__tests__/workspace.test.ts +0 -373
  65. package/src/verified-autofix/format-validator.ts +0 -517
  66. package/src/verified-autofix/index.ts +0 -63
  67. package/src/verified-autofix/pipeline.ts +0 -403
  68. package/src/verified-autofix/repo-fingerprint.ts +0 -459
  69. package/src/verified-autofix/workspace.ts +0 -531
  70. package/src/verified-autofix.ts +0 -1187
  71. package/src/visualization/dependency-graph.d.ts +0 -85
  72. package/src/visualization/dependency-graph.d.ts.map +0 -1
  73. package/src/visualization/dependency-graph.ts +0 -495
  74. 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
- }
@@ -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);