@vibecheckai/cli 3.0.10 → 3.1.1

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 (162) hide show
  1. package/bin/.generated +25 -0
  2. package/bin/dev/run-v2-torture.js +30 -30
  3. package/bin/registry.js +105 -0
  4. package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
  5. package/bin/runners/lib/analysis-core.js +271 -271
  6. package/bin/runners/lib/analyzers.js +579 -579
  7. package/bin/runners/lib/auth-truth.js +193 -193
  8. package/bin/runners/lib/backup.js +62 -62
  9. package/bin/runners/lib/billing.js +107 -107
  10. package/bin/runners/lib/claims.js +118 -118
  11. package/bin/runners/lib/cli-output.js +368 -0
  12. package/bin/runners/lib/cli-ui.js +540 -540
  13. package/bin/runners/lib/contracts/auth-contract.js +202 -202
  14. package/bin/runners/lib/contracts/env-contract.js +181 -181
  15. package/bin/runners/lib/contracts/external-contract.js +206 -206
  16. package/bin/runners/lib/contracts/guard.js +168 -168
  17. package/bin/runners/lib/contracts/index.js +89 -89
  18. package/bin/runners/lib/contracts/plan-validator.js +311 -311
  19. package/bin/runners/lib/contracts/route-contract.js +199 -199
  20. package/bin/runners/lib/contracts.js +804 -804
  21. package/bin/runners/lib/detect.js +89 -89
  22. package/bin/runners/lib/detectors-v2.js +703 -703
  23. package/bin/runners/lib/doctor/autofix.js +254 -254
  24. package/bin/runners/lib/doctor/index.js +37 -37
  25. package/bin/runners/lib/doctor/modules/dependencies.js +325 -325
  26. package/bin/runners/lib/doctor/modules/index.js +46 -46
  27. package/bin/runners/lib/doctor/modules/network.js +250 -250
  28. package/bin/runners/lib/doctor/modules/project.js +312 -312
  29. package/bin/runners/lib/doctor/modules/runtime.js +224 -224
  30. package/bin/runners/lib/doctor/modules/security.js +348 -348
  31. package/bin/runners/lib/doctor/modules/system.js +213 -213
  32. package/bin/runners/lib/doctor/modules/vibecheck.js +394 -394
  33. package/bin/runners/lib/doctor/reporter.js +262 -262
  34. package/bin/runners/lib/doctor/service.js +262 -262
  35. package/bin/runners/lib/doctor/types.js +113 -113
  36. package/bin/runners/lib/doctor/ui.js +263 -263
  37. package/bin/runners/lib/doctor-v2.js +608 -608
  38. package/bin/runners/lib/drift.js +425 -425
  39. package/bin/runners/lib/enforcement.js +72 -72
  40. package/bin/runners/lib/enterprise-detect.js +603 -603
  41. package/bin/runners/lib/enterprise-init.js +942 -942
  42. package/bin/runners/lib/entitlements-v2.js +490 -493
  43. package/bin/runners/lib/env-resolver.js +417 -417
  44. package/bin/runners/lib/env-template.js +66 -66
  45. package/bin/runners/lib/env.js +189 -189
  46. package/bin/runners/lib/extractors/client-calls.js +990 -990
  47. package/bin/runners/lib/extractors/fastify-route-dump.js +573 -573
  48. package/bin/runners/lib/extractors/fastify-routes.js +426 -426
  49. package/bin/runners/lib/extractors/index.js +363 -363
  50. package/bin/runners/lib/extractors/next-routes.js +524 -524
  51. package/bin/runners/lib/extractors/proof-graph.js +431 -431
  52. package/bin/runners/lib/extractors/route-matcher.js +451 -451
  53. package/bin/runners/lib/extractors/truthpack-v2.js +377 -377
  54. package/bin/runners/lib/extractors/ui-bindings.js +547 -547
  55. package/bin/runners/lib/findings-schema.js +281 -281
  56. package/bin/runners/lib/firewall-prompt.js +50 -50
  57. package/bin/runners/lib/graph/graph-builder.js +265 -265
  58. package/bin/runners/lib/graph/html-renderer.js +413 -413
  59. package/bin/runners/lib/graph/index.js +32 -32
  60. package/bin/runners/lib/graph/runtime-collector.js +215 -215
  61. package/bin/runners/lib/graph/static-extractor.js +518 -518
  62. package/bin/runners/lib/html-report.js +650 -650
  63. package/bin/runners/lib/init-wizard.js +308 -308
  64. package/bin/runners/lib/llm.js +75 -75
  65. package/bin/runners/lib/meter.js +61 -61
  66. package/bin/runners/lib/missions/evidence.js +126 -126
  67. package/bin/runners/lib/missions/plan.js +69 -69
  68. package/bin/runners/lib/missions/templates.js +192 -192
  69. package/bin/runners/lib/patch.js +40 -40
  70. package/bin/runners/lib/permissions/auth-model.js +213 -213
  71. package/bin/runners/lib/permissions/idor-prover.js +205 -205
  72. package/bin/runners/lib/permissions/index.js +45 -45
  73. package/bin/runners/lib/permissions/matrix-builder.js +198 -198
  74. package/bin/runners/lib/pkgjson.js +28 -28
  75. package/bin/runners/lib/policy.js +295 -295
  76. package/bin/runners/lib/preflight.js +142 -142
  77. package/bin/runners/lib/reality/correlation-detectors.js +359 -359
  78. package/bin/runners/lib/reality/index.js +318 -318
  79. package/bin/runners/lib/reality/request-hashing.js +416 -416
  80. package/bin/runners/lib/reality/request-mapper.js +453 -453
  81. package/bin/runners/lib/reality/safety-rails.js +463 -463
  82. package/bin/runners/lib/reality/semantic-snapshot.js +408 -408
  83. package/bin/runners/lib/reality/toast-detector.js +393 -393
  84. package/bin/runners/lib/reality-findings.js +84 -84
  85. package/bin/runners/lib/receipts.js +179 -0
  86. package/bin/runners/lib/redact.js +29 -29
  87. package/bin/runners/lib/replay/capsule-manager.js +154 -154
  88. package/bin/runners/lib/replay/index.js +263 -263
  89. package/bin/runners/lib/replay/player.js +348 -348
  90. package/bin/runners/lib/replay/recorder.js +331 -331
  91. package/bin/runners/lib/report-engine.js +447 -447
  92. package/bin/runners/lib/report-html.js +1499 -1499
  93. package/bin/runners/lib/report-templates.js +969 -969
  94. package/bin/runners/lib/report.js +135 -135
  95. package/bin/runners/lib/route-detection.js +1140 -1140
  96. package/bin/runners/lib/route-truth.js +477 -477
  97. package/bin/runners/lib/sandbox/index.js +59 -59
  98. package/bin/runners/lib/sandbox/proof-chain.js +399 -399
  99. package/bin/runners/lib/sandbox/sandbox-runner.js +205 -205
  100. package/bin/runners/lib/sandbox/worktree.js +174 -174
  101. package/bin/runners/lib/schema-validator.js +350 -350
  102. package/bin/runners/lib/schemas/contracts.schema.json +160 -160
  103. package/bin/runners/lib/schemas/finding.schema.json +100 -100
  104. package/bin/runners/lib/schemas/mission-pack.schema.json +206 -206
  105. package/bin/runners/lib/schemas/proof-graph.schema.json +176 -176
  106. package/bin/runners/lib/schemas/reality-report.schema.json +162 -162
  107. package/bin/runners/lib/schemas/share-pack.schema.json +180 -180
  108. package/bin/runners/lib/schemas/ship-report.schema.json +117 -117
  109. package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -303
  110. package/bin/runners/lib/schemas/validator.js +438 -438
  111. package/bin/runners/lib/score-history.js +282 -282
  112. package/bin/runners/lib/share-pack.js +239 -239
  113. package/bin/runners/lib/snippets.js +67 -67
  114. package/bin/runners/lib/truth.js +667 -667
  115. package/bin/runners/lib/upsell.js +510 -0
  116. package/bin/runners/lib/usage.js +153 -0
  117. package/bin/runners/lib/validate-patch.js +156 -156
  118. package/bin/runners/lib/verdict-engine.js +628 -628
  119. package/bin/runners/reality/engine.js +917 -917
  120. package/bin/runners/reality/flows.js +122 -122
  121. package/bin/runners/reality/report.js +378 -378
  122. package/bin/runners/reality/session.js +193 -193
  123. package/bin/runners/runAuth.js +51 -0
  124. package/bin/runners/runBadge.js +31 -4
  125. package/bin/runners/runClaimVerifier.js +483 -483
  126. package/bin/runners/runContext.js +56 -56
  127. package/bin/runners/runContextCompiler.js +385 -385
  128. package/bin/runners/runCtx.js +674 -674
  129. package/bin/runners/runCtxDiff.js +301 -301
  130. package/bin/runners/runCtxGuard.js +176 -176
  131. package/bin/runners/runCtxSync.js +116 -116
  132. package/bin/runners/runDoctor.js +72 -3
  133. package/bin/runners/runFix.js +13 -0
  134. package/bin/runners/runGate.js +17 -17
  135. package/bin/runners/runGraph.js +454 -440
  136. package/bin/runners/runGuard.js +168 -168
  137. package/bin/runners/runInitGha.js +164 -164
  138. package/bin/runners/runInstall.js +277 -277
  139. package/bin/runners/runInteractive.js +388 -388
  140. package/bin/runners/runLabs.js +340 -340
  141. package/bin/runners/runMcp.js +865 -42
  142. package/bin/runners/runMissionGenerator.js +282 -282
  143. package/bin/runners/runPR.js +255 -255
  144. package/bin/runners/runPermissions.js +304 -290
  145. package/bin/runners/runPreflight.js +580 -0
  146. package/bin/runners/runProve.js +1252 -1193
  147. package/bin/runners/runReality.js +1328 -1328
  148. package/bin/runners/runReplay.js +499 -499
  149. package/bin/runners/runReport.js +584 -584
  150. package/bin/runners/runShare.js +212 -212
  151. package/bin/runners/runShip.js +98 -19
  152. package/bin/runners/runStatus.js +138 -138
  153. package/bin/runners/runTruthpack.js +636 -636
  154. package/bin/runners/runVerify.js +272 -0
  155. package/bin/runners/runWatch.js +407 -407
  156. package/bin/vibecheck.js +110 -95
  157. package/mcp-server/consolidated-tools.js +804 -804
  158. package/mcp-server/tools/index.js +72 -72
  159. package/mcp-server/truth-context.js +581 -581
  160. package/mcp-server/truth-firewall-tools.js +1500 -1500
  161. package/package.json +1 -1
  162. package/bin/runners/runProof.zip +0 -0
@@ -1,493 +1,490 @@
1
- /**
2
- * Entitlements v2 - CANONICAL Tier Enforcement
3
- *
4
- * SINGLE SOURCE OF TRUTH for all tier gating in vibecheck CLI.
5
- * Every command runner MUST use this module for access control.
6
- *
7
- * NO BYPASS ALLOWED:
8
- * - No owner-mode env vars
9
- * - No offline fallback that grants paid features
10
- * - No silent feature access
11
- *
12
- * Exit Codes:
13
- * - 0: Success
14
- * - 2: BLOCK verdict (CI failure)
15
- * - 3: Feature not allowed (upgrade required)
16
- * - 4: Misconfiguration/env error
17
- *
18
- * Tiers:
19
- * - FREE ($0): Basic scanning and validation
20
- * - STARTER ($29/repo/mo): CI gates, launch, PR, badge, MCP
21
- * - PRO ($99/repo/mo): Full fix, prove, ai-test, share, advanced reality
22
- * - ENTERPRISE: Custom (placeholder only)
23
- */
24
-
25
- "use strict";
26
-
27
- const fs = require("fs");
28
- const path = require("path");
29
- const os = require("os");
30
- const crypto = require("crypto");
31
-
32
- // ═══════════════════════════════════════════════════════════════════════════════
33
- // EXIT CODES
34
- // ═══════════════════════════════════════════════════════════════════════════════
35
- const EXIT_SUCCESS = 0;
36
- const EXIT_BLOCK_VERDICT = 2;
37
- const EXIT_FEATURE_NOT_ALLOWED = 3;
38
- const EXIT_MISCONFIG = 4;
39
-
40
- // ═══════════════════════════════════════════════════════════════════════════════
41
- // TIER DEFINITIONS - SOURCE OF TRUTH
42
- // ═══════════════════════════════════════════════════════════════════════════════
43
- const TIERS = {
44
- free: { name: "FREE", price: 0, order: 0 },
45
- starter: { name: "STARTER", price: 29, order: 1 },
46
- pro: { name: "PRO", price: 99, order: 2 },
47
- enterprise: { name: "ENTERPRISE", price: 499, order: 3 },
48
- };
49
-
50
- // ═══════════════════════════════════════════════════════════════════════════════
51
- // ENTITLEMENTS MATRIX - SOURCE OF TRUTH
52
- // Format: feature -> { minTier, caps?, downgrade? }
53
- // ═══════════════════════════════════════════════════════════════════════════════
54
- const ENTITLEMENTS = {
55
- // Core commands
56
- "scan": { minTier: "free" },
57
- "ship": { minTier: "free", caps: { free: "static-only" } },
58
- "ship.static": { minTier: "free" },
59
- "ship.full": { minTier: "starter" },
60
-
61
- // Reality testing
62
- "reality": { minTier: "free", downgrade: "reality.preview" },
63
- "reality.preview": { minTier: "free", caps: { free: { maxPages: 5, maxClicks: 20, noAuthBoundary: true } } },
64
- "reality.full": { minTier: "starter" },
65
- "reality.advanced_auth_boundary": { minTier: "pro" },
66
-
67
- // Prove command
68
- "prove": { minTier: "pro" },
69
-
70
- // Fix command
71
- "fix": { minTier: "free", downgrade: "fix.plan_only" },
72
- "fix.plan_only": { minTier: "free" },
73
- "fix.apply_patches": { minTier: "pro" },
74
-
75
- // Report formats
76
- "report": { minTier: "free", downgrade: "report.html_md" },
77
- "report.html_md": { minTier: "free" },
78
- "report.sarif_csv": { minTier: "starter" },
79
- "report.compliance_packs": { minTier: "pro" },
80
-
81
- // Setup & DX
82
- "install": { minTier: "free" },
83
- "init": { minTier: "free" },
84
- "doctor": { minTier: "free" },
85
- "status": { minTier: "free" },
86
- "watch": { minTier: "free" },
87
-
88
- // AI Truth
89
- "ctx": { minTier: "free" },
90
- "guard": { minTier: "free" },
91
- "context": { minTier: "free" },
92
- "mdc": { minTier: "free" },
93
-
94
- // CI & Collaboration (STARTER+)
95
- "gate": { minTier: "starter" },
96
- "launch": { minTier: "starter" },
97
- "pr": { minTier: "starter" },
98
- "badge": { minTier: "starter" },
99
- "mcp": { minTier: "starter" },
100
-
101
- // PRO only
102
- "share": { minTier: "pro" },
103
- "ai-test": { minTier: "pro" },
104
-
105
- // Account (always free)
106
- "login": { minTier: "free" },
107
- "logout": { minTier: "free" },
108
- "whoami": { minTier: "free" },
109
-
110
- // Labs/experimental
111
- "labs": { minTier: "free" },
112
- };
113
-
114
- // ═══════════════════════════════════════════════════════════════════════════════
115
- // LIMITS BY TIER
116
- // ═══════════════════════════════════════════════════════════════════════════════
117
- const LIMITS = {
118
- free: {
119
- realityMaxPages: 5,
120
- realityMaxClicks: 20,
121
- realityAuthBoundary: false,
122
- reportFormats: ["html", "md"],
123
- fixApplyPatches: false,
124
- scansPerMonth: 50,
125
- shipChecksPerMonth: 20,
126
- },
127
- starter: {
128
- realityMaxPages: 50,
129
- realityMaxClicks: 500,
130
- realityAuthBoundary: true,
131
- reportFormats: ["html", "md", "sarif", "csv"],
132
- fixApplyPatches: false,
133
- scansPerMonth: -1, // unlimited
134
- shipChecksPerMonth: -1,
135
- },
136
- pro: {
137
- realityMaxPages: -1, // unlimited
138
- realityMaxClicks: -1,
139
- realityAuthBoundary: true,
140
- realityAdvancedAuth: true,
141
- reportFormats: ["html", "md", "sarif", "csv", "compliance"],
142
- fixApplyPatches: true,
143
- scansPerMonth: -1,
144
- shipChecksPerMonth: -1,
145
- },
146
- enterprise: {
147
- realityMaxPages: -1,
148
- realityMaxClicks: -1,
149
- realityAuthBoundary: true,
150
- realityAdvancedAuth: true,
151
- reportFormats: ["html", "md", "sarif", "csv", "compliance", "custom"],
152
- fixApplyPatches: true,
153
- scansPerMonth: -1,
154
- shipChecksPerMonth: -1,
155
- },
156
- };
157
-
158
- const API_BASE_URL = process.env.VIBECHECK_API_URL || "https://api.vibecheckai.dev";
159
-
160
- // ═══════════════════════════════════════════════════════════════════════════════
161
- // CACHE PATHS
162
- // ═══════════════════════════════════════════════════════════════════════════════
163
- function getEntitlementsCachePath(projectPath) {
164
- return path.join(projectPath || process.cwd(), ".vibecheck", ".entitlements.json");
165
- }
166
-
167
- function getGlobalConfigPath() {
168
- const home = os.homedir();
169
- if (process.platform === "win32") {
170
- return path.join(process.env.APPDATA || path.join(home, "AppData", "Roaming"), "vibecheck", "config.json");
171
- }
172
- return path.join(home, ".config", "vibecheck", "config.json");
173
- }
174
-
175
- // ═══════════════════════════════════════════════════════════════════════════════
176
- // TIER COMPARISON
177
- // ═══════════════════════════════════════════════════════════════════════════════
178
- function tierMeetsMinimum(currentTier, requiredTier) {
179
- const current = TIERS[currentTier]?.order ?? -1;
180
- const required = TIERS[requiredTier]?.order ?? 999;
181
- return current >= required;
182
- }
183
-
184
- function getTierLabel(tier) {
185
- return TIERS[tier]?.name || tier.toUpperCase();
186
- }
187
-
188
- // ═══════════════════════════════════════════════════════════════════════════════
189
- // CORE API: getTier()
190
- // ═══════════════════════════════════════════════════════════════════════════════
191
- let _cachedTier = null;
192
- let _cachedTierExpiry = 0;
193
-
194
- async function getTier(options = {}) {
195
- const { apiKey, projectPath, forceRefresh = false } = options;
196
-
197
- // Check cache (5 minute TTL)
198
- if (!forceRefresh && _cachedTier && Date.now() < _cachedTierExpiry) {
199
- return _cachedTier;
200
- }
201
-
202
- // No API key = free tier
203
- if (!apiKey) {
204
- _cachedTier = "free";
205
- _cachedTierExpiry = Date.now() + 300000;
206
- return "free";
207
- }
208
-
209
- // Fetch from API
210
- try {
211
- const res = await fetch(`${API_BASE_URL}/v1/entitlements`, {
212
- method: "GET",
213
- headers: { "Authorization": `Bearer ${apiKey}` },
214
- signal: AbortSignal.timeout(5000),
215
- });
216
-
217
- if (res.ok) {
218
- const data = await res.json();
219
- _cachedTier = data.tier || "free";
220
- _cachedTierExpiry = Date.now() + 300000;
221
-
222
- // Cache locally
223
- try {
224
- const cachePath = getEntitlementsCachePath(projectPath);
225
- const dir = path.dirname(cachePath);
226
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
227
- fs.writeFileSync(cachePath, JSON.stringify({ tier: _cachedTier, fetchedAt: new Date().toISOString() }, null, 2));
228
- } catch {}
229
-
230
- return _cachedTier;
231
- }
232
-
233
- if (res.status === 401) {
234
- return "free"; // Invalid key = free tier
235
- }
236
- } catch {
237
- // Network error - check local cache
238
- try {
239
- const cachePath = getEntitlementsCachePath(projectPath);
240
- if (fs.existsSync(cachePath)) {
241
- const cached = JSON.parse(fs.readFileSync(cachePath, "utf8"));
242
- // Only use cache if less than 24 hours old
243
- const fetchedAt = new Date(cached.fetchedAt).getTime();
244
- if (Date.now() - fetchedAt < 24 * 3600 * 1000) {
245
- return cached.tier || "free";
246
- }
247
- }
248
- } catch {}
249
- }
250
-
251
- // Default to free (no offline bypass to paid features)
252
- return "free";
253
- }
254
-
255
- // ═══════════════════════════════════════════════════════════════════════════════
256
- // CORE API: getLimits()
257
- // ═══════════════════════════════════════════════════════════════════════════════
258
- function getLimits(tier) {
259
- return LIMITS[tier] || LIMITS.free;
260
- }
261
-
262
- // ═══════════════════════════════════════════════════════════════════════════════
263
- // CORE API: enforce() - THE GATEKEEPER
264
- // ═══════════════════════════════════════════════════════════════════════════════
265
- /**
266
- * Enforce feature access. Returns enforcement result.
267
- *
268
- * @param {string} feature - Feature key (e.g., "prove", "fix.apply_patches")
269
- * @param {object} options - { apiKey?, projectPath?, silent? }
270
- * @returns {object} - { allowed, tier, downgrade?, exitCode, message }
271
- */
272
- async function enforce(feature, options = {}) {
273
- const { apiKey, projectPath, silent = false } = options;
274
-
275
- const tier = await getTier({ apiKey, projectPath });
276
- const entitlement = ENTITLEMENTS[feature];
277
-
278
- if (!entitlement) {
279
- // Unknown feature - block by default
280
- return {
281
- allowed: false,
282
- tier,
283
- exitCode: EXIT_MISCONFIG,
284
- message: `Unknown feature: ${feature}`,
285
- };
286
- }
287
-
288
- const hasAccess = tierMeetsMinimum(tier, entitlement.minTier);
289
-
290
- if (hasAccess) {
291
- // Full access
292
- return {
293
- allowed: true,
294
- tier,
295
- limits: getLimits(tier),
296
- caps: entitlement.caps?.[tier] || null,
297
- };
298
- }
299
-
300
- // Check for downgrade option
301
- if (entitlement.downgrade) {
302
- const downgradeEntitlement = ENTITLEMENTS[entitlement.downgrade];
303
- if (downgradeEntitlement && tierMeetsMinimum(tier, downgradeEntitlement.minTier)) {
304
- // Downgrade allowed
305
- const caps = downgradeEntitlement.caps?.[tier] || null;
306
- const message = formatDowngradeMessage(feature, entitlement.downgrade, tier, entitlement.minTier, caps);
307
-
308
- if (!silent) {
309
- console.log(message);
310
- }
311
-
312
- return {
313
- allowed: true,
314
- tier,
315
- downgrade: entitlement.downgrade,
316
- limits: getLimits(tier),
317
- caps,
318
- message,
319
- };
320
- }
321
- }
322
-
323
- // Not allowed - generate upgrade message
324
- const message = formatUpgradeMessage(feature, tier, entitlement.minTier);
325
-
326
- if (!silent) {
327
- console.error(message);
328
- }
329
-
330
- return {
331
- allowed: false,
332
- tier,
333
- requiredTier: entitlement.minTier,
334
- exitCode: EXIT_FEATURE_NOT_ALLOWED,
335
- message,
336
- };
337
- }
338
-
339
- // ═══════════════════════════════════════════════════════════════════════════════
340
- // MESSAGING
341
- // ═══════════════════════════════════════════════════════════════════════════════
342
- const c = {
343
- reset: "\x1b[0m",
344
- bold: "\x1b[1m",
345
- dim: "\x1b[2m",
346
- red: "\x1b[31m",
347
- green: "\x1b[32m",
348
- yellow: "\x1b[33m",
349
- cyan: "\x1b[36m",
350
- magenta: "\x1b[35m",
351
- };
352
-
353
- function formatUpgradeMessage(feature, currentTier, requiredTier) {
354
- const tierColors = { starter: c.cyan, pro: c.magenta, enterprise: c.yellow };
355
- const reqColor = tierColors[requiredTier] || c.yellow;
356
-
357
- return `
358
- ${c.red}${c.bold}⛔ Feature Not Available${c.reset}
359
-
360
- ${c.yellow}${feature}${c.reset} requires ${reqColor}${getTierLabel(requiredTier)}${c.reset} plan.
361
- Your current plan: ${c.dim}${getTierLabel(currentTier)}${c.reset}
362
-
363
- ${c.cyan}Upgrade at:${c.reset} https://vibecheckai.dev/pricing
364
-
365
- ${c.dim}Exit code: ${EXIT_FEATURE_NOT_ALLOWED}${c.reset}
366
- `;
367
- }
368
-
369
- function formatDowngradeMessage(feature, downgradeTo, currentTier, requiredTier, caps) {
370
- const tierColors = { starter: c.cyan, pro: c.magenta, enterprise: c.yellow };
371
- const reqColor = tierColors[requiredTier] || c.yellow;
372
-
373
- let capsStr = "";
374
- if (caps) {
375
- if (typeof caps === "string") {
376
- capsStr = ` ${c.dim}Mode: ${caps}${c.reset}\n`;
377
- } else if (typeof caps === "object") {
378
- const entries = Object.entries(caps).map(([k, v]) => `${k}: ${v}`).join(", ");
379
- capsStr = ` ${c.dim}Limits: ${entries}${c.reset}\n`;
380
- }
381
- }
382
-
383
- return `
384
- ${c.yellow}${c.bold}⚠ Running in Preview Mode${c.reset}
385
-
386
- Full ${c.yellow}${feature}${c.reset} requires ${reqColor}${getTierLabel(requiredTier)}${c.reset} plan.
387
- Running ${c.green}${downgradeTo}${c.reset} instead.
388
- ${capsStr}
389
- ${c.cyan}Upgrade for full access:${c.reset} https://vibecheckai.dev/pricing
390
- `;
391
- }
392
-
393
- // ═══════════════════════════════════════════════════════════════════════════════
394
- // CONVENIENCE HELPERS
395
- // ═══════════════════════════════════════════════════════════════════════════════
396
-
397
- /**
398
- * Check if a command is allowed (does not print messages)
399
- */
400
- async function checkCommand(command, options = {}) {
401
- return enforce(command, { ...options, silent: true });
402
- }
403
-
404
- /**
405
- * Enforce and exit if not allowed
406
- */
407
- async function enforceOrExit(feature, options = {}) {
408
- const result = await enforce(feature, options);
409
- if (!result.allowed) {
410
- process.exit(result.exitCode);
411
- }
412
- return result;
413
- }
414
-
415
- /**
416
- * Get the minimum tier required for a feature
417
- */
418
- function getMinTierForFeature(feature) {
419
- return ENTITLEMENTS[feature]?.minTier || "enterprise";
420
- }
421
-
422
- /**
423
- * Check if tier has access to feature (sync, for help display)
424
- */
425
- function tierHasFeature(tier, feature) {
426
- const entitlement = ENTITLEMENTS[feature];
427
- if (!entitlement) return false;
428
- return tierMeetsMinimum(tier, entitlement.minTier);
429
- }
430
-
431
- /**
432
- * Get all features for a tier
433
- */
434
- function getFeaturesForTier(tier) {
435
- const features = [];
436
- for (const [feature, def] of Object.entries(ENTITLEMENTS)) {
437
- if (tierMeetsMinimum(tier, def.minTier)) {
438
- features.push(feature);
439
- }
440
- }
441
- return features;
442
- }
443
-
444
- // ═══════════════════════════════════════════════════════════════════════════════
445
- // COMMAND GROUPING FOR HELP DISPLAY
446
- // ═══════════════════════════════════════════════════════════════════════════════
447
- const COMMAND_GROUPS = {
448
- "Proof Loop": ["scan", "ship", "reality", "prove", "fix", "report"],
449
- "Setup & DX": ["install", "init", "doctor", "status", "watch", "launch"],
450
- "AI Truth": ["ctx", "guard", "context", "mdc"],
451
- "CI & Collaboration": ["gate", "pr", "badge"],
452
- "Reporting": ["report"],
453
- "Automation": ["ai-test", "mcp", "share"],
454
- };
455
-
456
- function getCommandGroup(command) {
457
- for (const [group, commands] of Object.entries(COMMAND_GROUPS)) {
458
- if (commands.includes(command)) return group;
459
- }
460
- return "Other";
461
- }
462
-
463
- // ═══════════════════════════════════════════════════════════════════════════════
464
- // EXPORTS
465
- // ═══════════════════════════════════════════════════════════════════════════════
466
- module.exports = {
467
- // Core API
468
- getTier,
469
- getLimits,
470
- enforce,
471
- enforceOrExit,
472
- checkCommand,
473
-
474
- // Tier helpers
475
- tierMeetsMinimum,
476
- getTierLabel,
477
- getMinTierForFeature,
478
- tierHasFeature,
479
- getFeaturesForTier,
480
-
481
- // Command grouping
482
- COMMAND_GROUPS,
483
- getCommandGroup,
484
-
485
- // Constants
486
- TIERS,
487
- ENTITLEMENTS,
488
- LIMITS,
489
- EXIT_SUCCESS,
490
- EXIT_BLOCK_VERDICT,
491
- EXIT_FEATURE_NOT_ALLOWED,
492
- EXIT_MISCONFIG,
493
- };
1
+ /**
2
+ * Entitlements v2 - CANONICAL Tier Enforcement
3
+ *
4
+ * SINGLE SOURCE OF TRUTH for all tier gating in vibecheck CLI.
5
+ * Every command runner MUST use this module for access control.
6
+ *
7
+ * NO BYPASS ALLOWED:
8
+ * - No owner-mode env vars
9
+ * - No offline fallback that grants paid features
10
+ * - No silent feature access
11
+ *
12
+ * Exit Codes:
13
+ * - 0: Success
14
+ * - 2: BLOCK verdict (CI failure)
15
+ * - 3: Feature not allowed (upgrade required)
16
+ * - 4: Misconfiguration/env error
17
+ *
18
+ * Tiers:
19
+ * - FREE ($0): Basic scanning and validation
20
+ * - PRO ($99/repo/mo): Full fix, prove, ai-test, share, advanced reality
21
+ * - COMPLETE ($199/repo/mo): Everything including permissions, graph, advanced compliance
22
+ */
23
+
24
+ "use strict";
25
+
26
+ const fs = require("fs");
27
+ const path = require("path");
28
+ const os = require("os");
29
+ const crypto = require("crypto");
30
+
31
+ // ═══════════════════════════════════════════════════════════════════════════════
32
+ // EXIT CODES
33
+ // ═══════════════════════════════════════════════════════════════════════════════
34
+ const EXIT_SUCCESS = 0;
35
+ const EXIT_BLOCK_VERDICT = 2;
36
+ const EXIT_FEATURE_NOT_ALLOWED = 3;
37
+ const EXIT_MISCONFIG = 4;
38
+
39
+ // ═══════════════════════════════════════════════════════════════════════════════
40
+ // TIER DEFINITIONS - SOURCE OF TRUTH
41
+ // ═══════════════════════════════════════════════════════════════════════════════
42
+ const TIERS = {
43
+ free: { name: "FREE", price: 0, order: 0 },
44
+ starter: { name: "STARTER", price: 29, order: 1 },
45
+ pro: { name: "PRO", price: 99, order: 2 },
46
+ complete: { name: "COMPLETE", price: 199, order: 3 },
47
+ };
48
+
49
+ // ═══════════════════════════════════════════════════════════════════════════════
50
+ // ENTITLEMENTS MATRIX - SOURCE OF TRUTH
51
+ // Format: feature -> { minTier, caps?, downgrade? }
52
+ // ═══════════════════════════════════════════════════════════════════════════════
53
+ const ENTITLEMENTS = {
54
+ // Core commands
55
+ "scan": { minTier: "free" },
56
+ "ship": { minTier: "free", caps: { free: "static-only" } },
57
+ "ship.static": { minTier: "free" },
58
+ "ship.full": { minTier: "pro" },
59
+
60
+ // Reality testing
61
+ "reality": { minTier: "free", downgrade: "reality.preview" },
62
+ "reality.preview": { minTier: "free", caps: { free: { maxPages: 5, maxClicks: 20, noAuthBoundary: true } } },
63
+ "reality.full": { minTier: "pro" },
64
+ "reality.advanced_auth_boundary": { minTier: "complete" },
65
+
66
+ // Prove command
67
+ "prove": { minTier: "pro" },
68
+
69
+ // Fix command
70
+ "fix": { minTier: "free", downgrade: "fix.plan_only" },
71
+ "fix.plan_only": { minTier: "free" },
72
+ "fix.apply_patches": { minTier: "complete" },
73
+
74
+ // Report formats
75
+ "report": { minTier: "free", downgrade: "report.html_md" },
76
+ "report.html_md": { minTier: "free" },
77
+ "report.sarif_csv": { minTier: "pro" },
78
+ "report.compliance_packs": { minTier: "complete" },
79
+
80
+ // Setup & DX
81
+ "install": { minTier: "free" },
82
+ "init": { minTier: "free" },
83
+ "doctor": { minTier: "free" },
84
+ "status": { minTier: "free" },
85
+ "watch": { minTier: "free" },
86
+ "preflight": { minTier: "free" },
87
+
88
+ // AI Truth
89
+ "ctx": { minTier: "free" },
90
+ "guard": { minTier: "free" },
91
+ "context": { minTier: "free" },
92
+ "mdc": { minTier: "free" },
93
+
94
+ // PRO only
95
+ "replay": { minTier: "pro" },
96
+ "share": { minTier: "pro" },
97
+ "ai-test": { minTier: "pro" },
98
+
99
+ // STARTER and above
100
+ "gate": { minTier: "starter" },
101
+ "pr": { minTier: "starter" },
102
+ "badge": { minTier: "starter" },
103
+ "launch": { minTier: "starter" },
104
+ "mcp": { minTier: "starter", downgrade: "mcp.help_only" },
105
+ "mcp.help_only": { minTier: "free", caps: { free: "help and print-config only" } },
106
+
107
+ // COMPLETE only
108
+ "permissions": { minTier: "complete" },
109
+ "graph": { minTier: "complete" },
110
+
111
+ // Account (always free)
112
+ "login": { minTier: "free" },
113
+ "logout": { minTier: "free" },
114
+ "whoami": { minTier: "free" },
115
+
116
+ // Labs/experimental
117
+ "labs": { minTier: "free" },
118
+ };
119
+
120
+ // ═══════════════════════════════════════════════════════════════════════════════
121
+ // LIMITS BY TIER
122
+ // ═══════════════════════════════════════════════════════════════════════════════
123
+ const LIMITS = {
124
+ free: {
125
+ realityMaxPages: 5,
126
+ realityMaxClicks: 20,
127
+ realityAuthBoundary: false,
128
+ reportFormats: ["html", "md"],
129
+ fixApplyPatches: false,
130
+ scansPerMonth: 50,
131
+ shipChecksPerMonth: 20,
132
+ },
133
+ pro: {
134
+ realityMaxPages: -1, // unlimited
135
+ realityMaxClicks: -1,
136
+ realityAuthBoundary: true,
137
+ realityAdvancedAuth: false,
138
+ reportFormats: ["html", "md", "sarif", "csv"],
139
+ fixApplyPatches: false,
140
+ scansPerMonth: -1, // unlimited
141
+ shipChecksPerMonth: -1,
142
+ },
143
+ complete: {
144
+ realityMaxPages: -1,
145
+ realityMaxClicks: -1,
146
+ realityAuthBoundary: true,
147
+ realityAdvancedAuth: true,
148
+ reportFormats: ["html", "md", "sarif", "csv", "compliance"],
149
+ fixApplyPatches: true,
150
+ scansPerMonth: -1,
151
+ shipChecksPerMonth: -1,
152
+ },
153
+ };
154
+
155
+ const API_BASE_URL = process.env.VIBECHECK_API_URL || "https://api.vibecheckai.dev";
156
+
157
+ // ═══════════════════════════════════════════════════════════════════════════════
158
+ // CACHE PATHS
159
+ // ═══════════════════════════════════════════════════════════════════════════════
160
+ function getEntitlementsCachePath(projectPath) {
161
+ return path.join(projectPath || process.cwd(), ".vibecheck", ".entitlements.json");
162
+ }
163
+
164
+ function getGlobalConfigPath() {
165
+ const home = os.homedir();
166
+ if (process.platform === "win32") {
167
+ return path.join(process.env.APPDATA || path.join(home, "AppData", "Roaming"), "vibecheck", "config.json");
168
+ }
169
+ return path.join(home, ".config", "vibecheck", "config.json");
170
+ }
171
+
172
+ // ═══════════════════════════════════════════════════════════════════════════════
173
+ // TIER COMPARISON
174
+ // ═══════════════════════════════════════════════════════════════════════════════
175
+ function tierMeetsMinimum(currentTier, requiredTier) {
176
+ const current = TIERS[currentTier]?.order ?? -1;
177
+ const required = TIERS[requiredTier]?.order ?? 999;
178
+ return current >= required;
179
+ }
180
+
181
+ function getTierLabel(tier) {
182
+ return TIERS[tier]?.name || tier.toUpperCase();
183
+ }
184
+
185
+ // ═══════════════════════════════════════════════════════════════════════════════
186
+ // CORE API: getTier()
187
+ // ═══════════════════════════════════════════════════════════════════════════════
188
+ let _cachedTier = null;
189
+ let _cachedTierExpiry = 0;
190
+
191
+ async function getTier(options = {}) {
192
+ const { apiKey, projectPath, forceRefresh = false } = options;
193
+
194
+ // Check cache (5 minute TTL)
195
+ if (!forceRefresh && _cachedTier && Date.now() < _cachedTierExpiry) {
196
+ return _cachedTier;
197
+ }
198
+
199
+ // No API key = free tier
200
+ if (!apiKey) {
201
+ _cachedTier = "free";
202
+ _cachedTierExpiry = Date.now() + 300000;
203
+ return "free";
204
+ }
205
+
206
+ // Fetch from API
207
+ try {
208
+ const res = await fetch(`${API_BASE_URL}/v1/entitlements`, {
209
+ method: "GET",
210
+ headers: { "Authorization": `Bearer ${apiKey}` },
211
+ signal: AbortSignal.timeout(5000),
212
+ });
213
+
214
+ if (res.ok) {
215
+ const data = await res.json();
216
+ _cachedTier = data.tier || "free";
217
+ _cachedTierExpiry = Date.now() + 300000;
218
+
219
+ // Cache locally
220
+ try {
221
+ const cachePath = getEntitlementsCachePath(projectPath);
222
+ const dir = path.dirname(cachePath);
223
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
224
+ fs.writeFileSync(cachePath, JSON.stringify({ tier: _cachedTier, fetchedAt: new Date().toISOString() }, null, 2));
225
+ } catch {}
226
+
227
+ return _cachedTier;
228
+ }
229
+
230
+ if (res.status === 401) {
231
+ return "free"; // Invalid key = free tier
232
+ }
233
+ } catch {
234
+ // Network error - check local cache
235
+ try {
236
+ const cachePath = getEntitlementsCachePath(projectPath);
237
+ if (fs.existsSync(cachePath)) {
238
+ const cached = JSON.parse(fs.readFileSync(cachePath, "utf8"));
239
+ // Only use cache if less than 24 hours old
240
+ const fetchedAt = new Date(cached.fetchedAt).getTime();
241
+ if (Date.now() - fetchedAt < 24 * 3600 * 1000) {
242
+ return cached.tier || "free";
243
+ }
244
+ }
245
+ } catch {}
246
+ }
247
+
248
+ // Default to free (no offline bypass to paid features)
249
+ return "free";
250
+ }
251
+
252
+ // ═══════════════════════════════════════════════════════════════════════════════
253
+ // CORE API: getLimits()
254
+ // ═══════════════════════════════════════════════════════════════════════════════
255
+ function getLimits(tier) {
256
+ return LIMITS[tier] || LIMITS.free;
257
+ }
258
+
259
+ // ═══════════════════════════════════════════════════════════════════════════════
260
+ // CORE API: enforce() - THE GATEKEEPER
261
+ // ═══════════════════════════════════════════════════════════════════════════════
262
+ /**
263
+ * Enforce feature access. Returns enforcement result.
264
+ *
265
+ * @param {string} feature - Feature key (e.g., "prove", "fix.apply_patches")
266
+ * @param {object} options - { apiKey?, projectPath?, silent? }
267
+ * @returns {object} - { allowed, tier, downgrade?, exitCode, message }
268
+ */
269
+ async function enforce(feature, options = {}) {
270
+ const { apiKey, projectPath, silent = false } = options;
271
+
272
+ const tier = await getTier({ apiKey, projectPath });
273
+ const entitlement = ENTITLEMENTS[feature];
274
+
275
+ if (!entitlement) {
276
+ // Unknown feature - block by default
277
+ return {
278
+ allowed: false,
279
+ tier,
280
+ exitCode: EXIT_MISCONFIG,
281
+ message: `Unknown feature: ${feature}`,
282
+ };
283
+ }
284
+
285
+ const hasAccess = tierMeetsMinimum(tier, entitlement.minTier);
286
+
287
+ if (hasAccess) {
288
+ // Full access
289
+ return {
290
+ allowed: true,
291
+ tier,
292
+ limits: getLimits(tier),
293
+ caps: entitlement.caps?.[tier] || null,
294
+ };
295
+ }
296
+
297
+ // Check for downgrade option
298
+ if (entitlement.downgrade) {
299
+ const downgradeEntitlement = ENTITLEMENTS[entitlement.downgrade];
300
+ if (downgradeEntitlement && tierMeetsMinimum(tier, downgradeEntitlement.minTier)) {
301
+ // Downgrade allowed
302
+ const caps = downgradeEntitlement.caps?.[tier] || null;
303
+ const message = formatDowngradeMessage(feature, entitlement.downgrade, tier, entitlement.minTier, caps);
304
+
305
+ if (!silent) {
306
+ console.log(message);
307
+ }
308
+
309
+ return {
310
+ allowed: true,
311
+ tier,
312
+ downgrade: entitlement.downgrade,
313
+ limits: getLimits(tier),
314
+ caps,
315
+ message,
316
+ };
317
+ }
318
+ }
319
+
320
+ // Not allowed - generate upgrade message
321
+ const message = formatUpgradeMessage(feature, tier, entitlement.minTier);
322
+
323
+ if (!silent) {
324
+ console.error(message);
325
+ }
326
+
327
+ return {
328
+ allowed: false,
329
+ tier,
330
+ requiredTier: entitlement.minTier,
331
+ exitCode: EXIT_FEATURE_NOT_ALLOWED,
332
+ message,
333
+ };
334
+ }
335
+
336
+ // ═══════════════════════════════════════════════════════════════════════════════
337
+ // MESSAGING
338
+ // ═══════════════════════════════════════════════════════════════════════════════
339
+ const c = {
340
+ reset: "\x1b[0m",
341
+ bold: "\x1b[1m",
342
+ dim: "\x1b[2m",
343
+ red: "\x1b[31m",
344
+ green: "\x1b[32m",
345
+ yellow: "\x1b[33m",
346
+ cyan: "\x1b[36m",
347
+ magenta: "\x1b[35m",
348
+ };
349
+
350
+ function formatUpgradeMessage(feature, currentTier, requiredTier) {
351
+ const tierColors = { starter: c.cyan, pro: c.magenta, enterprise: c.yellow };
352
+ const reqColor = tierColors[requiredTier] || c.yellow;
353
+
354
+ return `
355
+ ${c.red}${c.bold}⛔ Feature Not Available${c.reset}
356
+
357
+ ${c.yellow}${feature}${c.reset} requires ${reqColor}${getTierLabel(requiredTier)}${c.reset} plan.
358
+ Your current plan: ${c.dim}${getTierLabel(currentTier)}${c.reset}
359
+
360
+ ${c.cyan}Upgrade at:${c.reset} https://vibecheckai.dev/pricing
361
+
362
+ ${c.dim}Exit code: ${EXIT_FEATURE_NOT_ALLOWED}${c.reset}
363
+ `;
364
+ }
365
+
366
+ function formatDowngradeMessage(feature, downgradeTo, currentTier, requiredTier, caps) {
367
+ const tierColors = { starter: c.cyan, pro: c.magenta, enterprise: c.yellow };
368
+ const reqColor = tierColors[requiredTier] || c.yellow;
369
+
370
+ let capsStr = "";
371
+ if (caps) {
372
+ if (typeof caps === "string") {
373
+ capsStr = ` ${c.dim}Mode: ${caps}${c.reset}\n`;
374
+ } else if (typeof caps === "object") {
375
+ const entries = Object.entries(caps).map(([k, v]) => `${k}: ${v}`).join(", ");
376
+ capsStr = ` ${c.dim}Limits: ${entries}${c.reset}\n`;
377
+ }
378
+ }
379
+
380
+ return `
381
+ ${c.yellow}${c.bold}⚠ Running in Preview Mode${c.reset}
382
+
383
+ Full ${c.yellow}${feature}${c.reset} requires ${reqColor}${getTierLabel(requiredTier)}${c.reset} plan.
384
+ Running ${c.green}${downgradeTo}${c.reset} instead.
385
+ ${capsStr}
386
+ ${c.cyan}Upgrade for full access:${c.reset} https://vibecheckai.dev/pricing
387
+ `;
388
+ }
389
+
390
+ // ═══════════════════════════════════════════════════════════════════════════════
391
+ // CONVENIENCE HELPERS
392
+ // ═══════════════════════════════════════════════════════════════════════════════
393
+
394
+ /**
395
+ * Check if a command is allowed (does not print messages)
396
+ */
397
+ async function checkCommand(command, options = {}) {
398
+ return enforce(command, { ...options, silent: true });
399
+ }
400
+
401
+ /**
402
+ * Enforce and exit if not allowed
403
+ */
404
+ async function enforceOrExit(feature, options = {}) {
405
+ const result = await enforce(feature, options);
406
+ if (!result.allowed) {
407
+ process.exit(result.exitCode);
408
+ }
409
+ return result;
410
+ }
411
+
412
+ /**
413
+ * Get the minimum tier required for a feature
414
+ */
415
+ function getMinTierForFeature(feature) {
416
+ return ENTITLEMENTS[feature]?.minTier || "enterprise";
417
+ }
418
+
419
+ /**
420
+ * Check if tier has access to feature (sync, for help display)
421
+ */
422
+ function tierHasFeature(tier, feature) {
423
+ const entitlement = ENTITLEMENTS[feature];
424
+ if (!entitlement) return false;
425
+ return tierMeetsMinimum(tier, entitlement.minTier);
426
+ }
427
+
428
+ /**
429
+ * Get all features for a tier
430
+ */
431
+ function getFeaturesForTier(tier) {
432
+ const features = [];
433
+ for (const [feature, def] of Object.entries(ENTITLEMENTS)) {
434
+ if (tierMeetsMinimum(tier, def.minTier)) {
435
+ features.push(feature);
436
+ }
437
+ }
438
+ return features;
439
+ }
440
+
441
+ // ═══════════════════════════════════════════════════════════════════════════════
442
+ // COMMAND GROUPING FOR HELP DISPLAY
443
+ // ═══════════════════════════════════════════════════════════════════════════════
444
+ const COMMAND_GROUPS = {
445
+ "Proof Loop": ["scan", "ship", "reality", "prove", "fix", "report"],
446
+ "Setup & DX": ["install", "init", "doctor", "status", "watch", "launch"],
447
+ "AI Truth": ["ctx", "guard", "context", "mdc"],
448
+ "CI & Collaboration": ["gate", "pr", "badge"],
449
+ "Reporting": ["report"],
450
+ "Automation": ["ai-test", "mcp", "share"],
451
+ };
452
+
453
+ function getCommandGroup(command) {
454
+ for (const [group, commands] of Object.entries(COMMAND_GROUPS)) {
455
+ if (commands.includes(command)) return group;
456
+ }
457
+ return "Other";
458
+ }
459
+
460
+ // ═══════════════════════════════════════════════════════════════════════════════
461
+ // EXPORTS
462
+ // ═══════════════════════════════════════════════════════════════════════════════
463
+ module.exports = {
464
+ // Core API
465
+ getTier,
466
+ getLimits,
467
+ enforce,
468
+ enforceOrExit,
469
+ checkCommand,
470
+
471
+ // Tier helpers
472
+ tierMeetsMinimum,
473
+ getTierLabel,
474
+ getMinTierForFeature,
475
+ tierHasFeature,
476
+ getFeaturesForTier,
477
+
478
+ // Command grouping
479
+ COMMAND_GROUPS,
480
+ getCommandGroup,
481
+
482
+ // Constants
483
+ TIERS,
484
+ ENTITLEMENTS,
485
+ LIMITS,
486
+ EXIT_SUCCESS,
487
+ EXIT_BLOCK_VERDICT,
488
+ EXIT_FEATURE_NOT_ALLOWED,
489
+ EXIT_MISCONFIG,
490
+ };