@vibecheckai/cli 3.7.0 → 3.8.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/README.md +135 -63
- package/bin/_deprecations.js +447 -19
- package/bin/_router.js +1 -1
- package/bin/registry.js +347 -280
- package/bin/runners/context/generators/cursor-enhanced.js +2439 -0
- package/bin/runners/lib/agent-firewall/enforcement/gateway.js +1059 -0
- package/bin/runners/lib/agent-firewall/enforcement/index.js +98 -0
- package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -0
- package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -0
- package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/change-event.schema.json +173 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/intent.schema.json +181 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/verdict.schema.json +222 -0
- package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -0
- package/bin/runners/lib/agent-firewall/index.js +200 -0
- package/bin/runners/lib/agent-firewall/integration/index.js +20 -0
- package/bin/runners/lib/agent-firewall/integration/ship-gate.js +437 -0
- package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +622 -0
- package/bin/runners/lib/agent-firewall/intent/auto-detect.js +426 -0
- package/bin/runners/lib/agent-firewall/intent/index.js +102 -0
- package/bin/runners/lib/agent-firewall/intent/schema.js +352 -0
- package/bin/runners/lib/agent-firewall/intent/store.js +283 -0
- package/bin/runners/lib/agent-firewall/interception/fs-interceptor.js +502 -0
- package/bin/runners/lib/agent-firewall/interception/index.js +23 -0
- package/bin/runners/lib/agent-firewall/session/collector.js +451 -0
- package/bin/runners/lib/agent-firewall/session/index.js +26 -0
- package/bin/runners/lib/artifact-envelope.js +540 -0
- package/bin/runners/lib/auth-shared.js +977 -0
- package/bin/runners/lib/checkpoint.js +941 -0
- package/bin/runners/lib/cleanup/engine.js +571 -0
- package/bin/runners/lib/cleanup/index.js +53 -0
- package/bin/runners/lib/cleanup/output.js +375 -0
- package/bin/runners/lib/cleanup/rules.js +1060 -0
- package/bin/runners/lib/doctor/diagnosis-receipt.js +454 -0
- package/bin/runners/lib/doctor/failure-signatures.js +526 -0
- package/bin/runners/lib/doctor/fix-script.js +336 -0
- package/bin/runners/lib/doctor/modules/build-tools.js +453 -0
- package/bin/runners/lib/doctor/modules/index.js +62 -3
- package/bin/runners/lib/doctor/modules/os-quirks.js +706 -0
- package/bin/runners/lib/doctor/modules/repo-integrity.js +485 -0
- package/bin/runners/lib/doctor/safe-repair.js +384 -0
- package/bin/runners/lib/engines/attack-detector.js +1192 -0
- package/bin/runners/lib/entitlements-v2.js +2 -2
- package/bin/runners/lib/missions/briefing.js +427 -0
- package/bin/runners/lib/missions/checkpoint.js +753 -0
- package/bin/runners/lib/missions/hardening.js +851 -0
- package/bin/runners/lib/missions/plan.js +421 -32
- package/bin/runners/lib/missions/safety-gates.js +645 -0
- package/bin/runners/lib/missions/schema.js +478 -0
- package/bin/runners/lib/packs/bundle.js +675 -0
- package/bin/runners/lib/packs/evidence-pack.js +671 -0
- package/bin/runners/lib/packs/pack-factory.js +837 -0
- package/bin/runners/lib/packs/permissions-pack.js +686 -0
- package/bin/runners/lib/packs/proof-graph-pack.js +779 -0
- package/bin/runners/lib/safelist/index.js +96 -0
- package/bin/runners/lib/safelist/integration.js +334 -0
- package/bin/runners/lib/safelist/matcher.js +696 -0
- package/bin/runners/lib/safelist/schema.js +948 -0
- package/bin/runners/lib/safelist/store.js +438 -0
- package/bin/runners/lib/schemas/ship-manifest.schema.json +251 -0
- package/bin/runners/lib/ship-gate.js +832 -0
- package/bin/runners/lib/ship-manifest.js +1153 -0
- package/bin/runners/lib/ship-output.js +1 -1
- package/bin/runners/lib/unified-cli-output.js +710 -383
- package/bin/runners/lib/upsell.js +3 -3
- package/bin/runners/lib/why-tree.js +650 -0
- package/bin/runners/runAllowlist.js +33 -4
- package/bin/runners/runApprove.js +240 -1122
- package/bin/runners/runAudit.js +692 -0
- package/bin/runners/runAuth.js +325 -29
- package/bin/runners/runCheckpoint.js +442 -494
- package/bin/runners/runCleanup.js +343 -0
- package/bin/runners/runDoctor.js +269 -19
- package/bin/runners/runFix.js +411 -32
- package/bin/runners/runForge.js +411 -0
- package/bin/runners/runIntent.js +906 -0
- package/bin/runners/runKickoff.js +878 -0
- package/bin/runners/runLaunch.js +2000 -0
- package/bin/runners/runLink.js +785 -0
- package/bin/runners/runMcp.js +1741 -837
- package/bin/runners/runPacks.js +2089 -0
- package/bin/runners/runPolish.js +41 -0
- package/bin/runners/runSafelist.js +1190 -0
- package/bin/runners/runScan.js +21 -9
- package/bin/runners/runShield.js +1282 -0
- package/bin/runners/runShip.js +395 -16
- package/bin/vibecheck.js +34 -6
- package/mcp-server/README.md +117 -158
- package/mcp-server/handlers/tool-handler.ts +3 -3
- package/mcp-server/index.js +16 -0
- package/mcp-server/intent-firewall-interceptor.js +529 -0
- package/mcp-server/manifest.json +473 -0
- package/mcp-server/package.json +1 -1
- package/mcp-server/registry/tool-registry.js +315 -523
- package/mcp-server/registry/tools.json +442 -428
- package/mcp-server/tier-auth.js +68 -11
- package/mcp-server/tools-v3.js +70 -16
- package/package.json +1 -1
- package/bin/runners/runProof.zip +0 -0
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Auth Module Bridge (JavaScript)
|
|
3
|
+
*
|
|
4
|
+
* Provides shared auth functionality for CLI commands.
|
|
5
|
+
* This bridges the TypeScript shared/auth module for JS consumers.
|
|
6
|
+
*
|
|
7
|
+
* @module lib/auth-shared
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
const os = require("os");
|
|
13
|
+
const crypto = require("crypto");
|
|
14
|
+
|
|
15
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
16
|
+
// PATHS
|
|
17
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
18
|
+
|
|
19
|
+
const VIBECHECK_DIR = ".vibecheck";
|
|
20
|
+
const AUTH_FILE = "auth.json";
|
|
21
|
+
const CACHE_FILE = "entitlements-cache.json";
|
|
22
|
+
const AUDIT_FILE = "auth-audit.log";
|
|
23
|
+
const ACTIVITY_FILE = ".auth-activity.json";
|
|
24
|
+
const RATE_LIMIT_FILE = ".rate-limit.json";
|
|
25
|
+
|
|
26
|
+
// Rate limit state in memory
|
|
27
|
+
let rateLimitState = { retryAfter: null, timestamp: null };
|
|
28
|
+
|
|
29
|
+
// Constants
|
|
30
|
+
const MAX_RETRIES = 3;
|
|
31
|
+
const BASE_RETRY_DELAY_MS = 1000;
|
|
32
|
+
const FAILED_ATTEMPT_THRESHOLD = 5;
|
|
33
|
+
const LOCKOUT_DURATION_MS = 15 * 60 * 1000; // 15 minutes
|
|
34
|
+
const MAX_AUDIT_SIZE = 1024 * 1024; // 1MB
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get vibecheck config directory.
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
function getConfigDir() {
|
|
41
|
+
return path.join(os.homedir(), VIBECHECK_DIR);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get auth file path.
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
function getAuthPath() {
|
|
49
|
+
return path.join(getConfigDir(), AUTH_FILE);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get cache file path.
|
|
54
|
+
* @returns {string}
|
|
55
|
+
*/
|
|
56
|
+
function getCachePath() {
|
|
57
|
+
return path.join(getConfigDir(), CACHE_FILE);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get legacy config path (for migration).
|
|
62
|
+
* @returns {string}
|
|
63
|
+
*/
|
|
64
|
+
function getLegacyConfigPath() {
|
|
65
|
+
const home = os.homedir();
|
|
66
|
+
if (process.platform === "win32") {
|
|
67
|
+
return path.join(
|
|
68
|
+
process.env.APPDATA || path.join(home, "AppData", "Roaming"),
|
|
69
|
+
"vibecheck",
|
|
70
|
+
"config.json"
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return path.join(home, ".config", "vibecheck", "config.json");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
77
|
+
// REDACTION
|
|
78
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Mask API key for secure display.
|
|
82
|
+
* @param {string} key
|
|
83
|
+
* @returns {string}
|
|
84
|
+
*/
|
|
85
|
+
function redactApiKey(key) {
|
|
86
|
+
if (!key || typeof key !== "string" || key.length < 8) {
|
|
87
|
+
return "****";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// New format: grl_ prefix
|
|
91
|
+
if (key.startsWith("grl_")) {
|
|
92
|
+
return `grl_${"*".repeat(8)}${key.slice(-4)}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Legacy format: gr_<tier>_ prefix
|
|
96
|
+
const legacyMatch = key.match(/^(gr_[a-z]+_)/);
|
|
97
|
+
if (legacyMatch) {
|
|
98
|
+
return `${legacyMatch[1]}${"*".repeat(8)}${key.slice(-4)}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Unknown format
|
|
102
|
+
if (key.length >= 12) {
|
|
103
|
+
return `${key.slice(0, 3)}****${key.slice(-4)}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return "****";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Redact API keys in output text.
|
|
111
|
+
* @param {string} text
|
|
112
|
+
* @returns {string}
|
|
113
|
+
*/
|
|
114
|
+
function redactOutput(text) {
|
|
115
|
+
if (!text || typeof text !== "string") return text;
|
|
116
|
+
|
|
117
|
+
return text
|
|
118
|
+
.replace(/grl_[a-zA-Z0-9]{10,}/g, (m) => redactApiKey(m))
|
|
119
|
+
.replace(/gr_[a-z]+_[a-zA-Z0-9]{10,}/g, (m) => redactApiKey(m))
|
|
120
|
+
.replace(
|
|
121
|
+
/VIBECHECK_API_KEY\s*=\s*["']?([a-zA-Z0-9_-]{10,})["']?/gi,
|
|
122
|
+
(_, k) => `VIBECHECK_API_KEY=${redactApiKey(k)}`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
127
|
+
// VALIDATION
|
|
128
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Validate API key format.
|
|
132
|
+
* @param {string} key
|
|
133
|
+
* @returns {{ valid: boolean, error?: string, format?: string }}
|
|
134
|
+
*/
|
|
135
|
+
function validateKeyFormat(key) {
|
|
136
|
+
if (!key || typeof key !== "string") {
|
|
137
|
+
return { valid: false, error: "API key is required" };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const trimmed = key.trim();
|
|
141
|
+
|
|
142
|
+
if (trimmed.length < 20) {
|
|
143
|
+
return { valid: false, error: "API key is too short (minimum 20 characters)" };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// New format: grl_ prefix
|
|
147
|
+
if (trimmed.startsWith("grl_")) {
|
|
148
|
+
if (!/^grl_[a-zA-Z0-9]+$/.test(trimmed)) {
|
|
149
|
+
return { valid: false, error: "API key contains invalid characters", format: "grl" };
|
|
150
|
+
}
|
|
151
|
+
return { valid: true, format: "grl" };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Legacy format: gr_<tier>_ prefix
|
|
155
|
+
if (trimmed.startsWith("gr_")) {
|
|
156
|
+
if (!/^gr_[a-z]+_[a-zA-Z0-9]+$/.test(trimmed)) {
|
|
157
|
+
return { valid: false, error: "Invalid legacy API key format", format: "legacy" };
|
|
158
|
+
}
|
|
159
|
+
return { valid: true, format: "legacy" };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Also accept generic alphanumeric keys (backward compat)
|
|
163
|
+
if (/^[a-zA-Z0-9_-]+$/.test(trimmed) && trimmed.length >= 20) {
|
|
164
|
+
return { valid: true, format: "unknown" };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { valid: false, error: 'API key must start with "grl_"' };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Quick format check.
|
|
172
|
+
* @param {string} key
|
|
173
|
+
* @returns {boolean}
|
|
174
|
+
*/
|
|
175
|
+
function isValidKeyFormat(key) {
|
|
176
|
+
return validateKeyFormat(key).valid;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
180
|
+
// STORAGE
|
|
181
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Ensure config directory exists.
|
|
185
|
+
*/
|
|
186
|
+
function ensureConfigDir() {
|
|
187
|
+
const dir = getConfigDir();
|
|
188
|
+
if (!fs.existsSync(dir)) {
|
|
189
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Read credentials from new auth.json.
|
|
195
|
+
* @returns {object|null}
|
|
196
|
+
*/
|
|
197
|
+
function readCredentials() {
|
|
198
|
+
try {
|
|
199
|
+
const authPath = getAuthPath();
|
|
200
|
+
if (fs.existsSync(authPath)) {
|
|
201
|
+
return JSON.parse(fs.readFileSync(authPath, "utf8"));
|
|
202
|
+
}
|
|
203
|
+
} catch {
|
|
204
|
+
// Ignore
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Write credentials to auth.json.
|
|
211
|
+
* @param {object} credentials
|
|
212
|
+
*/
|
|
213
|
+
function writeCredentials(credentials) {
|
|
214
|
+
ensureConfigDir();
|
|
215
|
+
const authPath = getAuthPath();
|
|
216
|
+
const data = { version: 2, ...credentials };
|
|
217
|
+
fs.writeFileSync(authPath, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Delete credentials.
|
|
222
|
+
*/
|
|
223
|
+
function deleteCredentials() {
|
|
224
|
+
try {
|
|
225
|
+
const authPath = getAuthPath();
|
|
226
|
+
if (fs.existsSync(authPath)) {
|
|
227
|
+
fs.unlinkSync(authPath);
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
// Ignore
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Read entitlements cache.
|
|
236
|
+
* @returns {object|null}
|
|
237
|
+
*/
|
|
238
|
+
function readCache() {
|
|
239
|
+
try {
|
|
240
|
+
const cachePath = getCachePath();
|
|
241
|
+
if (fs.existsSync(cachePath)) {
|
|
242
|
+
return JSON.parse(fs.readFileSync(cachePath, "utf8"));
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
// Ignore
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Write entitlements cache.
|
|
252
|
+
* @param {object} cache
|
|
253
|
+
*/
|
|
254
|
+
function writeCache(cache) {
|
|
255
|
+
ensureConfigDir();
|
|
256
|
+
const cachePath = getCachePath();
|
|
257
|
+
fs.writeFileSync(cachePath, JSON.stringify(cache, null, 2), { mode: 0o600 });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Delete cache.
|
|
262
|
+
*/
|
|
263
|
+
function deleteCache() {
|
|
264
|
+
try {
|
|
265
|
+
const cachePath = getCachePath();
|
|
266
|
+
if (fs.existsSync(cachePath)) {
|
|
267
|
+
fs.unlinkSync(cachePath);
|
|
268
|
+
}
|
|
269
|
+
} catch {
|
|
270
|
+
// Ignore
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Clear all auth data (logout).
|
|
276
|
+
*/
|
|
277
|
+
function clearAll() {
|
|
278
|
+
deleteCredentials();
|
|
279
|
+
deleteCache();
|
|
280
|
+
|
|
281
|
+
// Clear activity tracker
|
|
282
|
+
try {
|
|
283
|
+
const activityPath = path.join(getConfigDir(), ACTIVITY_FILE);
|
|
284
|
+
if (fs.existsSync(activityPath)) {
|
|
285
|
+
fs.unlinkSync(activityPath);
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
// Ignore
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Clear rate limit
|
|
292
|
+
try {
|
|
293
|
+
const ratePath = path.join(getConfigDir(), RATE_LIMIT_FILE);
|
|
294
|
+
if (fs.existsSync(ratePath)) {
|
|
295
|
+
fs.unlinkSync(ratePath);
|
|
296
|
+
}
|
|
297
|
+
} catch {
|
|
298
|
+
// Ignore
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Also clear legacy locations
|
|
302
|
+
try {
|
|
303
|
+
const legacyPath = getLegacyConfigPath();
|
|
304
|
+
if (fs.existsSync(legacyPath)) {
|
|
305
|
+
const config = JSON.parse(fs.readFileSync(legacyPath, "utf8"));
|
|
306
|
+
delete config.apiKey;
|
|
307
|
+
fs.writeFileSync(legacyPath, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
308
|
+
}
|
|
309
|
+
} catch {
|
|
310
|
+
// Ignore
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
auditLog("LOGOUT", null, "cli");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
317
|
+
// AUDIT LOGGING
|
|
318
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Write audit log entry.
|
|
322
|
+
* @param {string} event
|
|
323
|
+
* @param {string|null} apiKey
|
|
324
|
+
* @param {string} source
|
|
325
|
+
* @param {object} [details]
|
|
326
|
+
*/
|
|
327
|
+
function auditLog(event, apiKey, source, details) {
|
|
328
|
+
try {
|
|
329
|
+
const configDir = getConfigDir();
|
|
330
|
+
const auditPath = path.join(configDir, AUDIT_FILE);
|
|
331
|
+
|
|
332
|
+
ensureConfigDir();
|
|
333
|
+
|
|
334
|
+
// Rotate log if too large
|
|
335
|
+
try {
|
|
336
|
+
const stats = fs.statSync(auditPath);
|
|
337
|
+
if (stats.size > MAX_AUDIT_SIZE) {
|
|
338
|
+
const backupPath = auditPath + ".1";
|
|
339
|
+
if (fs.existsSync(backupPath)) {
|
|
340
|
+
fs.unlinkSync(backupPath);
|
|
341
|
+
}
|
|
342
|
+
fs.renameSync(auditPath, backupPath);
|
|
343
|
+
}
|
|
344
|
+
} catch {
|
|
345
|
+
// File doesn't exist yet
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const entry = {
|
|
349
|
+
timestamp: new Date().toISOString(),
|
|
350
|
+
event,
|
|
351
|
+
keyMasked: apiKey ? redactApiKey(apiKey) : "****",
|
|
352
|
+
source,
|
|
353
|
+
details: details || undefined,
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
fs.appendFileSync(auditPath, JSON.stringify(entry) + "\n", { mode: 0o600 });
|
|
357
|
+
} catch {
|
|
358
|
+
// Audit logging should never break auth flow
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
363
|
+
// RATE LIMITING
|
|
364
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Check if currently rate limited.
|
|
368
|
+
* @returns {{ limited: boolean, retryAfter?: number }}
|
|
369
|
+
*/
|
|
370
|
+
function checkRateLimit() {
|
|
371
|
+
if (rateLimitState.retryAfter && rateLimitState.timestamp) {
|
|
372
|
+
const elapsed = Date.now() - rateLimitState.timestamp;
|
|
373
|
+
const remaining = (rateLimitState.retryAfter * 1000) - elapsed;
|
|
374
|
+
|
|
375
|
+
if (remaining > 0) {
|
|
376
|
+
return { limited: true, retryAfter: Math.ceil(remaining / 1000) };
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return { limited: false };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Record rate limit from response.
|
|
384
|
+
* @param {number} retryAfter
|
|
385
|
+
*/
|
|
386
|
+
function recordRateLimit(retryAfter) {
|
|
387
|
+
rateLimitState = { retryAfter, timestamp: Date.now() };
|
|
388
|
+
auditLog("RATE_LIMITED", null, "api", { retryAfter });
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Clear rate limit state.
|
|
393
|
+
*/
|
|
394
|
+
function clearRateLimitState() {
|
|
395
|
+
rateLimitState = { retryAfter: null, timestamp: null };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
399
|
+
// ACTIVITY TRACKING (LOCKOUT PROTECTION)
|
|
400
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Track failed auth attempt.
|
|
404
|
+
* @returns {{ locked: boolean, lockoutRemaining?: number }}
|
|
405
|
+
*/
|
|
406
|
+
function trackFailedAttempt() {
|
|
407
|
+
try {
|
|
408
|
+
const activityPath = path.join(getConfigDir(), ACTIVITY_FILE);
|
|
409
|
+
|
|
410
|
+
let activity = { failedAttempts: 0, lastAttempt: 0 };
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
activity = JSON.parse(fs.readFileSync(activityPath, "utf8"));
|
|
414
|
+
} catch {
|
|
415
|
+
// File doesn't exist
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Check if currently locked out
|
|
419
|
+
if (activity.lockoutUntil && Date.now() < activity.lockoutUntil) {
|
|
420
|
+
return {
|
|
421
|
+
locked: true,
|
|
422
|
+
lockoutRemaining: Math.ceil((activity.lockoutUntil - Date.now()) / 1000)
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Reset if lock expired
|
|
427
|
+
if (activity.lockoutUntil && Date.now() >= activity.lockoutUntil) {
|
|
428
|
+
activity = { failedAttempts: 0, lastAttempt: 0 };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Track new attempt
|
|
432
|
+
activity.failedAttempts++;
|
|
433
|
+
activity.lastAttempt = Date.now();
|
|
434
|
+
|
|
435
|
+
// Check if should lock out
|
|
436
|
+
if (activity.failedAttempts >= FAILED_ATTEMPT_THRESHOLD) {
|
|
437
|
+
activity.lockoutUntil = Date.now() + LOCKOUT_DURATION_MS;
|
|
438
|
+
auditLog("SUSPICIOUS_ACTIVITY", null, "local", {
|
|
439
|
+
reason: "Too many failed attempts",
|
|
440
|
+
attempts: activity.failedAttempts,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
fs.writeFileSync(activityPath, JSON.stringify(activity), { mode: 0o600 });
|
|
445
|
+
|
|
446
|
+
if (activity.lockoutUntil) {
|
|
447
|
+
return { locked: true, lockoutRemaining: Math.ceil(LOCKOUT_DURATION_MS / 1000) };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return { locked: false };
|
|
451
|
+
} catch {
|
|
452
|
+
return { locked: false };
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Reset activity tracker on successful auth.
|
|
458
|
+
*/
|
|
459
|
+
function resetActivityTracker() {
|
|
460
|
+
try {
|
|
461
|
+
const activityPath = path.join(getConfigDir(), ACTIVITY_FILE);
|
|
462
|
+
if (fs.existsSync(activityPath)) {
|
|
463
|
+
fs.unlinkSync(activityPath);
|
|
464
|
+
}
|
|
465
|
+
} catch {
|
|
466
|
+
// Non-critical
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
471
|
+
// INPUT SANITIZATION
|
|
472
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Deeply sanitize user input.
|
|
476
|
+
* @param {*} input
|
|
477
|
+
* @returns {string}
|
|
478
|
+
*/
|
|
479
|
+
function sanitizeInput(input) {
|
|
480
|
+
if (input === null || input === undefined) return "";
|
|
481
|
+
|
|
482
|
+
let str = String(input);
|
|
483
|
+
|
|
484
|
+
// Remove null bytes
|
|
485
|
+
str = str.replace(/\0/g, "");
|
|
486
|
+
|
|
487
|
+
// Remove control characters
|
|
488
|
+
str = str.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
|
|
489
|
+
|
|
490
|
+
// Trim whitespace
|
|
491
|
+
str = str.trim();
|
|
492
|
+
|
|
493
|
+
// Limit length
|
|
494
|
+
if (str.length > 256) {
|
|
495
|
+
str = str.slice(0, 256);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return str;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
502
|
+
// ENVIRONMENT DETECTION
|
|
503
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Detect execution environment.
|
|
507
|
+
* @returns {object}
|
|
508
|
+
*/
|
|
509
|
+
function detectEnvironment() {
|
|
510
|
+
const env = process.env;
|
|
511
|
+
|
|
512
|
+
let isCI = env.CI === "true" || env.CI === "1";
|
|
513
|
+
let ciProvider;
|
|
514
|
+
|
|
515
|
+
if (isCI) {
|
|
516
|
+
if (env.GITHUB_ACTIONS) ciProvider = "github";
|
|
517
|
+
else if (env.GITLAB_CI) ciProvider = "gitlab";
|
|
518
|
+
else if (env.CIRCLECI) ciProvider = "circleci";
|
|
519
|
+
else ciProvider = "unknown";
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
isCI,
|
|
524
|
+
ciProvider,
|
|
525
|
+
isProduction: env.NODE_ENV === "production",
|
|
526
|
+
isDevelopment: env.NODE_ENV === "development" || (!env.NODE_ENV && !isCI),
|
|
527
|
+
isInteractive: process.stdin.isTTY && !isCI,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Check if dev bypass is allowed.
|
|
533
|
+
* @returns {boolean}
|
|
534
|
+
*/
|
|
535
|
+
function isDevBypassAllowed() {
|
|
536
|
+
const env = detectEnvironment();
|
|
537
|
+
if (env.isProduction || env.isCI) return false;
|
|
538
|
+
return env.isDevelopment && process.env.VIBECHECK_DEV_PRO === "1";
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
542
|
+
// RESOLUTION
|
|
543
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Hash API key for cache.
|
|
547
|
+
* @param {string} key
|
|
548
|
+
* @returns {string}
|
|
549
|
+
*/
|
|
550
|
+
function hashApiKey(key) {
|
|
551
|
+
return crypto.createHash("sha256").update(key).digest("hex").slice(0, 16);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Resolve API key from all sources.
|
|
556
|
+
* @param {string} [directKey] Key passed via --key flag
|
|
557
|
+
* @returns {{ key: string|null, source: string }}
|
|
558
|
+
*/
|
|
559
|
+
function resolveApiKey(directKey) {
|
|
560
|
+
// 1. Direct argument
|
|
561
|
+
if (directKey && isValidKeyFormat(directKey)) {
|
|
562
|
+
return { key: directKey.trim(), source: "args" };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// 2. Environment variable
|
|
566
|
+
const envKey = process.env.VIBECHECK_API_KEY;
|
|
567
|
+
if (envKey && isValidKeyFormat(envKey)) {
|
|
568
|
+
return { key: envKey.trim(), source: "env" };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// 3. New auth.json
|
|
572
|
+
const creds = readCredentials();
|
|
573
|
+
if (creds?.apiKey && isValidKeyFormat(creds.apiKey)) {
|
|
574
|
+
return { key: creds.apiKey, source: "file" };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// 4. Legacy config.json
|
|
578
|
+
try {
|
|
579
|
+
const legacyPath = getLegacyConfigPath();
|
|
580
|
+
if (fs.existsSync(legacyPath)) {
|
|
581
|
+
const config = JSON.parse(fs.readFileSync(legacyPath, "utf8"));
|
|
582
|
+
if (config.apiKey && isValidKeyFormat(config.apiKey)) {
|
|
583
|
+
return { key: config.apiKey, source: "legacy" };
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
} catch {
|
|
587
|
+
// Ignore
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return { key: null, source: "none" };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Format source for display.
|
|
595
|
+
* @param {string} source
|
|
596
|
+
* @returns {string}
|
|
597
|
+
*/
|
|
598
|
+
function formatSource(source) {
|
|
599
|
+
switch (source) {
|
|
600
|
+
case "env": return "Environment Variable";
|
|
601
|
+
case "file": return "Config File";
|
|
602
|
+
case "args": return "Command Line";
|
|
603
|
+
case "legacy": return "Legacy Config";
|
|
604
|
+
case "none": return "Not Found";
|
|
605
|
+
default: return source;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
610
|
+
// CACHE HELPERS
|
|
611
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
612
|
+
|
|
613
|
+
const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
614
|
+
const STALE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Check if cache is valid.
|
|
618
|
+
* @param {object} cache
|
|
619
|
+
* @param {string} keyHash
|
|
620
|
+
* @returns {boolean}
|
|
621
|
+
*/
|
|
622
|
+
function isCacheValid(cache, keyHash) {
|
|
623
|
+
if (!cache || cache.keyHash !== keyHash) return false;
|
|
624
|
+
return Date.now() - cache.timestamp < CACHE_TTL_MS;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Check if cache is stale but usable.
|
|
629
|
+
* @param {object} cache
|
|
630
|
+
* @param {string} keyHash
|
|
631
|
+
* @returns {boolean}
|
|
632
|
+
*/
|
|
633
|
+
function isCacheStale(cache, keyHash) {
|
|
634
|
+
if (!cache || cache.keyHash !== keyHash) return false;
|
|
635
|
+
const age = Date.now() - cache.timestamp;
|
|
636
|
+
return age >= CACHE_TTL_MS && age < STALE_TTL_MS;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Get cache age in seconds.
|
|
641
|
+
* @param {object} cache
|
|
642
|
+
* @returns {number}
|
|
643
|
+
*/
|
|
644
|
+
function getCacheAge(cache) {
|
|
645
|
+
if (!cache) return 0;
|
|
646
|
+
return Math.floor((Date.now() - cache.timestamp) / 1000);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
650
|
+
// AUTH CHECK
|
|
651
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Sleep helper.
|
|
655
|
+
* @param {number} ms
|
|
656
|
+
* @returns {Promise<void>}
|
|
657
|
+
*/
|
|
658
|
+
function sleep(ms) {
|
|
659
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Calculate retry delay with exponential backoff.
|
|
664
|
+
* @param {number} attempt
|
|
665
|
+
* @returns {number}
|
|
666
|
+
*/
|
|
667
|
+
function getRetryDelay(attempt) {
|
|
668
|
+
const exponentialDelay = BASE_RETRY_DELAY_MS * Math.pow(2, attempt);
|
|
669
|
+
const jitter = Math.random() * 500;
|
|
670
|
+
return Math.min(exponentialDelay + jitter, 30000);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Fetch with retry and rate limit handling.
|
|
675
|
+
* @param {string} apiKey
|
|
676
|
+
* @param {number} [attempt]
|
|
677
|
+
* @returns {Promise<object>}
|
|
678
|
+
*/
|
|
679
|
+
async function fetchWithRetry(apiKey, attempt = 0) {
|
|
680
|
+
const apiUrl = process.env.VIBECHECK_API_URL || "https://api.vibecheckai.dev";
|
|
681
|
+
|
|
682
|
+
// Check rate limit
|
|
683
|
+
const rateCheck = checkRateLimit();
|
|
684
|
+
if (rateCheck.limited) {
|
|
685
|
+
return { ok: false, error: { code: "RATE_LIMITED", retryAfter: rateCheck.retryAfter } };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
try {
|
|
689
|
+
const response = await fetch(`${apiUrl}/v1/auth/whoami`, {
|
|
690
|
+
headers: {
|
|
691
|
+
Authorization: `Bearer ${apiKey}`,
|
|
692
|
+
"X-Client-Version": "2.0.0",
|
|
693
|
+
},
|
|
694
|
+
signal: AbortSignal.timeout(10000),
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
// Handle rate limiting
|
|
698
|
+
if (response.status === 429) {
|
|
699
|
+
const retryAfter = parseInt(response.headers.get("Retry-After") || "60", 10);
|
|
700
|
+
recordRateLimit(retryAfter);
|
|
701
|
+
return { ok: false, error: { code: "RATE_LIMITED", retryAfter } };
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Auth errors - don't retry
|
|
705
|
+
if (response.status === 401) {
|
|
706
|
+
trackFailedAttempt();
|
|
707
|
+
auditLog("KEY_INVALID", apiKey, "api");
|
|
708
|
+
return { ok: false, error: { code: "INVALID_API_KEY" } };
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (response.status === 403) {
|
|
712
|
+
auditLog("KEY_REVOKED", apiKey, "api");
|
|
713
|
+
return { ok: false, error: { code: "KEY_REVOKED" } };
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Server errors - retry
|
|
717
|
+
if (response.status >= 500 && attempt < MAX_RETRIES) {
|
|
718
|
+
await sleep(getRetryDelay(attempt));
|
|
719
|
+
return fetchWithRetry(apiKey, attempt + 1);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (!response.ok) {
|
|
723
|
+
return { ok: false, error: { code: "API_ERROR", status: response.status } };
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
clearRateLimitState();
|
|
727
|
+
resetActivityTracker();
|
|
728
|
+
|
|
729
|
+
const data = await response.json();
|
|
730
|
+
auditLog("KEY_VALIDATED", apiKey, "api");
|
|
731
|
+
return { ok: true, data };
|
|
732
|
+
} catch (err) {
|
|
733
|
+
// Network error - retry
|
|
734
|
+
if (attempt < MAX_RETRIES) {
|
|
735
|
+
await sleep(getRetryDelay(attempt));
|
|
736
|
+
return fetchWithRetry(apiKey, attempt + 1);
|
|
737
|
+
}
|
|
738
|
+
return { ok: false, error: { code: "NETWORK_ERROR" } };
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Comprehensive auth check (for --check flag).
|
|
744
|
+
* @param {string} [directKey]
|
|
745
|
+
* @returns {Promise<object>}
|
|
746
|
+
*/
|
|
747
|
+
async function checkAuth(directKey) {
|
|
748
|
+
// Check for lockout first
|
|
749
|
+
const lockout = trackFailedAttempt();
|
|
750
|
+
if (lockout.locked) {
|
|
751
|
+
// Reset the failed attempt we just added since we're checking lockout
|
|
752
|
+
resetActivityTracker();
|
|
753
|
+
return {
|
|
754
|
+
authenticated: false,
|
|
755
|
+
tier: "free",
|
|
756
|
+
source: "none",
|
|
757
|
+
keyMasked: "****",
|
|
758
|
+
networkOk: false,
|
|
759
|
+
cacheValid: false,
|
|
760
|
+
error: {
|
|
761
|
+
code: "LOCKED_OUT",
|
|
762
|
+
message: `Too many failed attempts. Try again in ${lockout.lockoutRemaining}s`,
|
|
763
|
+
recovery: `Wait ${Math.ceil(lockout.lockoutRemaining / 60)} minutes before retrying`,
|
|
764
|
+
},
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
resetActivityTracker(); // Reset the test attempt we just made
|
|
768
|
+
|
|
769
|
+
const resolved = resolveApiKey(directKey);
|
|
770
|
+
|
|
771
|
+
// No key found
|
|
772
|
+
if (!resolved.key) {
|
|
773
|
+
return {
|
|
774
|
+
authenticated: false,
|
|
775
|
+
tier: "free",
|
|
776
|
+
source: "none",
|
|
777
|
+
keyMasked: "****",
|
|
778
|
+
networkOk: false,
|
|
779
|
+
cacheValid: false,
|
|
780
|
+
error: {
|
|
781
|
+
code: "AUTH_REQUIRED",
|
|
782
|
+
message: "Not logged in",
|
|
783
|
+
recovery: 'Run "vibecheck login" or set VIBECHECK_API_KEY',
|
|
784
|
+
},
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const keyHash = hashApiKey(resolved.key);
|
|
789
|
+
const cache = readCache();
|
|
790
|
+
const creds = readCredentials();
|
|
791
|
+
|
|
792
|
+
// Check cache status
|
|
793
|
+
const cacheValid = cache ? isCacheValid(cache, keyHash) : false;
|
|
794
|
+
const cacheAge = cache ? getCacheAge(cache) : undefined;
|
|
795
|
+
|
|
796
|
+
// Try to validate with API (with retry)
|
|
797
|
+
const result = await fetchWithRetry(resolved.key);
|
|
798
|
+
|
|
799
|
+
let networkOk = result.ok;
|
|
800
|
+
let entitlements = result.data || null;
|
|
801
|
+
let error = null;
|
|
802
|
+
|
|
803
|
+
if (!result.ok) {
|
|
804
|
+
error = {
|
|
805
|
+
code: result.error?.code || "UNKNOWN",
|
|
806
|
+
message: getErrorMessage(result.error?.code),
|
|
807
|
+
recovery: getErrorRecovery(result.error?.code),
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
// Use cache if available
|
|
811
|
+
if (cache && (isCacheValid(cache, keyHash) || isCacheStale(cache, keyHash))) {
|
|
812
|
+
entitlements = cache.data;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Map plan to tier
|
|
817
|
+
const tier = entitlements?.plan === "free" || !entitlements?.plan ? "free" : "pro";
|
|
818
|
+
|
|
819
|
+
return {
|
|
820
|
+
authenticated: !!entitlements || !!creds?.apiKey,
|
|
821
|
+
tier: entitlements?.tier || tier,
|
|
822
|
+
source: resolved.source,
|
|
823
|
+
keyMasked: redactApiKey(resolved.key),
|
|
824
|
+
networkOk,
|
|
825
|
+
cacheValid,
|
|
826
|
+
cacheAge,
|
|
827
|
+
user: entitlements?.user || creds?.user,
|
|
828
|
+
entitlements,
|
|
829
|
+
error,
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Get human-readable error message.
|
|
835
|
+
* @param {string} code
|
|
836
|
+
* @returns {string}
|
|
837
|
+
*/
|
|
838
|
+
function getErrorMessage(code) {
|
|
839
|
+
switch (code) {
|
|
840
|
+
case "RATE_LIMITED": return "Rate limit exceeded";
|
|
841
|
+
case "INVALID_API_KEY": return "Invalid API key";
|
|
842
|
+
case "KEY_REVOKED": return "API key has been revoked";
|
|
843
|
+
case "KEY_EXPIRED": return "API key has expired";
|
|
844
|
+
case "NETWORK_ERROR": return "Cannot connect to API";
|
|
845
|
+
case "LOCKED_OUT": return "Too many failed attempts";
|
|
846
|
+
default: return "Authentication error";
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Get error recovery message.
|
|
852
|
+
* @param {string} code
|
|
853
|
+
* @returns {string}
|
|
854
|
+
*/
|
|
855
|
+
function getErrorRecovery(code) {
|
|
856
|
+
switch (code) {
|
|
857
|
+
case "RATE_LIMITED": return "Wait and try again";
|
|
858
|
+
case "INVALID_API_KEY": return "Check your API key format";
|
|
859
|
+
case "KEY_REVOKED": return "Generate a new key at vibecheckai.dev/settings/keys";
|
|
860
|
+
case "KEY_EXPIRED": return "Generate a new key at vibecheckai.dev/settings/keys";
|
|
861
|
+
case "NETWORK_ERROR": return "Check your network. Offline mode uses cached data.";
|
|
862
|
+
case "LOCKED_OUT": return "Wait before retrying";
|
|
863
|
+
default: return 'Run "vibecheck login" to re-authenticate';
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Refresh entitlements (for --refresh flag).
|
|
869
|
+
* @param {string} [directKey]
|
|
870
|
+
* @returns {Promise<object|null>}
|
|
871
|
+
*/
|
|
872
|
+
async function refreshEntitlements(directKey) {
|
|
873
|
+
const resolved = resolveApiKey(directKey);
|
|
874
|
+
|
|
875
|
+
if (!resolved.key) {
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const keyHash = hashApiKey(resolved.key);
|
|
880
|
+
|
|
881
|
+
try {
|
|
882
|
+
const apiUrl = process.env.VIBECHECK_API_URL || "https://api.vibecheckai.dev";
|
|
883
|
+
const response = await fetch(`${apiUrl}/v1/auth/whoami`, {
|
|
884
|
+
headers: { Authorization: `Bearer ${resolved.key}` },
|
|
885
|
+
signal: AbortSignal.timeout(10000),
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
if (!response.ok) {
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const entitlements = await response.json();
|
|
893
|
+
|
|
894
|
+
// Update cache
|
|
895
|
+
writeCache({
|
|
896
|
+
keyHash,
|
|
897
|
+
timestamp: Date.now(),
|
|
898
|
+
data: entitlements,
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
// Update stored credentials
|
|
902
|
+
const creds = readCredentials() || { version: 2, keySource: "file" };
|
|
903
|
+
writeCredentials({
|
|
904
|
+
...creds,
|
|
905
|
+
user: entitlements.user,
|
|
906
|
+
tier: entitlements.tier || (entitlements.plan === "free" ? "free" : "pro"),
|
|
907
|
+
lastValidated: new Date().toISOString(),
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
return entitlements;
|
|
911
|
+
} catch {
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
917
|
+
// EXPORTS
|
|
918
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
919
|
+
|
|
920
|
+
module.exports = {
|
|
921
|
+
// Paths
|
|
922
|
+
getConfigDir,
|
|
923
|
+
getAuthPath,
|
|
924
|
+
getCachePath,
|
|
925
|
+
getLegacyConfigPath,
|
|
926
|
+
|
|
927
|
+
// Redaction
|
|
928
|
+
redactApiKey,
|
|
929
|
+
redactOutput,
|
|
930
|
+
|
|
931
|
+
// Validation
|
|
932
|
+
validateKeyFormat,
|
|
933
|
+
isValidKeyFormat,
|
|
934
|
+
sanitizeInput,
|
|
935
|
+
|
|
936
|
+
// Storage
|
|
937
|
+
ensureConfigDir,
|
|
938
|
+
readCredentials,
|
|
939
|
+
writeCredentials,
|
|
940
|
+
deleteCredentials,
|
|
941
|
+
readCache,
|
|
942
|
+
writeCache,
|
|
943
|
+
deleteCache,
|
|
944
|
+
clearAll,
|
|
945
|
+
|
|
946
|
+
// Resolution
|
|
947
|
+
hashApiKey,
|
|
948
|
+
resolveApiKey,
|
|
949
|
+
formatSource,
|
|
950
|
+
|
|
951
|
+
// Cache
|
|
952
|
+
isCacheValid,
|
|
953
|
+
isCacheStale,
|
|
954
|
+
getCacheAge,
|
|
955
|
+
CACHE_TTL_MS,
|
|
956
|
+
STALE_TTL_MS,
|
|
957
|
+
|
|
958
|
+
// Rate limiting
|
|
959
|
+
checkRateLimit,
|
|
960
|
+
recordRateLimit,
|
|
961
|
+
clearRateLimitState,
|
|
962
|
+
|
|
963
|
+
// Activity tracking
|
|
964
|
+
trackFailedAttempt,
|
|
965
|
+
resetActivityTracker,
|
|
966
|
+
|
|
967
|
+
// Audit
|
|
968
|
+
auditLog,
|
|
969
|
+
|
|
970
|
+
// Environment
|
|
971
|
+
detectEnvironment,
|
|
972
|
+
isDevBypassAllowed,
|
|
973
|
+
|
|
974
|
+
// Auth
|
|
975
|
+
checkAuth,
|
|
976
|
+
refreshEntitlements,
|
|
977
|
+
};
|