@girardmedia/bootspring 2.2.0 → 2.3.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 (50) hide show
  1. package/README.md +2 -2
  2. package/bin/bootspring.js +35 -96
  3. package/claude-commands/agent.md +34 -0
  4. package/claude-commands/bs.md +31 -0
  5. package/claude-commands/build.md +25 -0
  6. package/claude-commands/skill.md +31 -0
  7. package/claude-commands/todo.md +25 -0
  8. package/dist/cli/index.cjs +17808 -0
  9. package/dist/core/index.d.ts +5814 -0
  10. package/dist/core.js +5780 -0
  11. package/dist/mcp/index.d.ts +1 -0
  12. package/dist/mcp-server.js +2299 -0
  13. package/generators/api-docs.js +2 -2
  14. package/generators/decisions.js +3 -3
  15. package/generators/health.js +16 -16
  16. package/generators/sprint.js +2 -2
  17. package/package.json +27 -59
  18. package/core/api-client.d.ts +0 -69
  19. package/core/api-client.js +0 -1482
  20. package/core/auth.d.ts +0 -98
  21. package/core/auth.js +0 -737
  22. package/core/build-orchestrator.js +0 -508
  23. package/core/build-state.js +0 -612
  24. package/core/config.d.ts +0 -106
  25. package/core/config.js +0 -1328
  26. package/core/context-loader.js +0 -580
  27. package/core/context.d.ts +0 -61
  28. package/core/context.js +0 -327
  29. package/core/entitlements.d.ts +0 -70
  30. package/core/entitlements.js +0 -322
  31. package/core/index.d.ts +0 -53
  32. package/core/index.js +0 -62
  33. package/core/mcp-config.js +0 -115
  34. package/core/policies.d.ts +0 -43
  35. package/core/policies.js +0 -113
  36. package/core/policy-matrix.js +0 -303
  37. package/core/project-activity.js +0 -175
  38. package/core/redaction.d.ts +0 -5
  39. package/core/redaction.js +0 -63
  40. package/core/self-update.js +0 -259
  41. package/core/session.js +0 -353
  42. package/core/task-extractor.js +0 -1098
  43. package/core/telemetry.d.ts +0 -55
  44. package/core/telemetry.js +0 -617
  45. package/core/tier-enforcement.js +0 -928
  46. package/core/utils.d.ts +0 -90
  47. package/core/utils.js +0 -455
  48. package/core/validation.js +0 -572
  49. package/mcp/server.d.ts +0 -57
  50. package/mcp/server.js +0 -264
package/core/auth.js DELETED
@@ -1,737 +0,0 @@
1
- /**
2
- * Bootspring Auth Module
3
- *
4
- * Manages authentication tokens stored in ~/.bootspring/
5
- */
6
-
7
- const fs = require('fs');
8
- const path = require('path');
9
- const os = require('os');
10
- const crypto = require('crypto');
11
-
12
- const BOOTSPRING_DIR = path.join(os.homedir(), '.bootspring');
13
- const CREDENTIALS_FILE = path.join(BOOTSPRING_DIR, 'credentials.json');
14
- const CONFIG_FILE = path.join(BOOTSPRING_DIR, 'config.json');
15
- const DEVICE_FILE = path.join(BOOTSPRING_DIR, 'device.json');
16
-
17
- // Simple encryption key derived from machine ID
18
- const getEncryptionKey = () => {
19
- const machineId = os.hostname() + os.userInfo().username;
20
- return crypto.createHash('sha256').update(machineId).digest();
21
- };
22
-
23
- /**
24
- * Ensure ~/.bootspring directory exists
25
- */
26
- function ensureDir() {
27
- if (!fs.existsSync(BOOTSPRING_DIR)) {
28
- fs.mkdirSync(BOOTSPRING_DIR, { recursive: true, mode: 0o700 });
29
- }
30
- }
31
-
32
- /**
33
- * Encrypt data
34
- * @throws {Error} If encryption fails - never falls back to plaintext
35
- */
36
- function encrypt(data) {
37
- const key = getEncryptionKey();
38
- const iv = crypto.randomBytes(16);
39
- const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
40
- let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex');
41
- encrypted += cipher.final('hex');
42
- return { iv: iv.toString('hex'), data: encrypted, v: 1 };
43
- }
44
-
45
- /**
46
- * Decrypt data
47
- * @throws {Error} If decryption fails - indicates corrupted or tampered credentials
48
- */
49
- function decrypt(encrypted) {
50
- // Check if data is in encrypted format (has iv, data, and optionally v)
51
- if (!encrypted || typeof encrypted !== 'object' || !encrypted.iv || !encrypted.data) {
52
- // Legacy unencrypted data - migrate on next save but don't fail
53
- // This handles the transition from old plaintext storage
54
- if (encrypted && typeof encrypted === 'object' && (encrypted.token || encrypted.apiKey)) {
55
- console.error('[bootspring] Migrating legacy unencrypted credentials to encrypted storage');
56
- return encrypted;
57
- }
58
- throw new Error('Invalid credential format');
59
- }
60
-
61
- const key = getEncryptionKey();
62
- const iv = Buffer.from(encrypted.iv, 'hex');
63
- const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
64
- let decrypted = decipher.update(encrypted.data, 'hex', 'utf8');
65
- decrypted += decipher.final('utf8');
66
- return JSON.parse(decrypted);
67
- }
68
-
69
- /**
70
- * Get stored credentials
71
- * @returns {object|null} Decrypted credentials or null if not available
72
- */
73
- function getCredentials() {
74
- try {
75
- if (fs.existsSync(CREDENTIALS_FILE)) {
76
- const raw = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf-8'));
77
- const decrypted = decrypt(raw);
78
-
79
- // If we got legacy unencrypted data, re-encrypt it immediately
80
- if (raw && typeof raw === 'object' && !raw.iv && !raw.data) {
81
- saveCredentials(decrypted);
82
- }
83
-
84
- return decrypted;
85
- }
86
- } catch (err) {
87
- // Log decryption failures for debugging but don't expose details
88
- console.error('[bootspring] Failed to read credentials:', err.message);
89
- }
90
- return null;
91
- }
92
-
93
- /**
94
- * Save credentials (encrypted)
95
- * @param {object} credentials - Credentials to save
96
- * @throws {Error} If encryption or file write fails
97
- */
98
- function saveCredentials(credentials) {
99
- ensureDir();
100
-
101
- // Never store API keys in credentials - only tokens
102
- const sanitized = { ...credentials };
103
- if (sanitized.apiKey) {
104
- // API keys should only be in memory or env vars, not on disk
105
- console.error('[bootspring] Warning: API keys should not be stored in credentials. Use project-scoped tokens instead.');
106
- delete sanitized.apiKey;
107
- }
108
-
109
- const encrypted = encrypt(sanitized);
110
- fs.writeFileSync(
111
- CREDENTIALS_FILE,
112
- JSON.stringify(encrypted, null, 2),
113
- { mode: 0o600 }
114
- );
115
- }
116
-
117
- /**
118
- * Clear credentials (logout)
119
- */
120
- function clearCredentials() {
121
- try {
122
- if (fs.existsSync(CREDENTIALS_FILE)) {
123
- fs.unlinkSync(CREDENTIALS_FILE);
124
- }
125
- } catch {
126
- // Ignore delete errors
127
- }
128
- }
129
-
130
- /**
131
- * Get auth token (JWT)
132
- */
133
- function getToken() {
134
- const creds = getCredentials();
135
- if (!creds) return null;
136
-
137
- // Check if token is expired
138
- if (creds.expiresAt && new Date(creds.expiresAt) < new Date()) {
139
- return null; // Token expired
140
- }
141
-
142
- return creds.token;
143
- }
144
-
145
- /**
146
- * Check if the current token is expiring soon
147
- * @param {number} [thresholdMs=300000] - Threshold in milliseconds (default 5 minutes)
148
- * @returns {{ expiringSoon: boolean, expiresAt: string|null, msUntilExpiry: number }}
149
- */
150
- function getTokenExpiryStatus(thresholdMs = 5 * 60 * 1000) {
151
- const creds = getCredentials();
152
- if (!creds || !creds.expiresAt) {
153
- return { expiringSoon: false, expiresAt: null, msUntilExpiry: 0 };
154
- }
155
-
156
- const expiresAt = new Date(creds.expiresAt).getTime();
157
- const now = Date.now();
158
- const msUntilExpiry = expiresAt - now;
159
-
160
- return {
161
- expiringSoon: msUntilExpiry > 0 && msUntilExpiry <= thresholdMs,
162
- expired: msUntilExpiry <= 0,
163
- expiresAt: creds.expiresAt,
164
- msUntilExpiry: Math.max(0, msUntilExpiry)
165
- };
166
- }
167
-
168
- /**
169
- * Get API key from project's .bootspring.json (for AI assistant sharing)
170
- */
171
- function findNearestProjectConfigPath() {
172
- let dir = process.cwd();
173
- for (let i = 0; i < 10; i++) {
174
- const configPath = path.join(dir, '.bootspring.json');
175
- if (fs.existsSync(configPath)) {
176
- return configPath;
177
- }
178
- const parent = path.dirname(dir);
179
- if (parent === dir) break;
180
- dir = parent;
181
- }
182
- return null;
183
- }
184
-
185
- function readNearestProjectConfig() {
186
- try {
187
- const configPath = findNearestProjectConfigPath();
188
- if (!configPath) {
189
- return null;
190
- }
191
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
192
- return { path: configPath, config };
193
- } catch {
194
- // Ignore errors
195
- }
196
- return null;
197
- }
198
-
199
- function writeNearestProjectConfig(configPath, config) {
200
- try {
201
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
202
- return true;
203
- } catch {
204
- return false;
205
- }
206
- }
207
-
208
- function getProjectScopedSessionState() {
209
- const projectConfig = readNearestProjectConfig();
210
- if (!projectConfig || !projectConfig.config.projectAuth) {
211
- return null;
212
- }
213
-
214
- const projectAuth = projectConfig.config.projectAuth;
215
- if (
216
- typeof projectAuth.token !== 'string' ||
217
- projectAuth.token.length === 0 ||
218
- typeof projectAuth.expiresAt !== 'string' ||
219
- projectAuth.expiresAt.length === 0
220
- ) {
221
- return null;
222
- }
223
-
224
- return {
225
- token: projectAuth.token,
226
- expiresAt: projectAuth.expiresAt,
227
- issuedAt: typeof projectAuth.issuedAt === 'string' ? projectAuth.issuedAt : new Date().toISOString(),
228
- source: typeof projectAuth.source === 'string' ? projectAuth.source : undefined,
229
- migratedFromLegacyApiKey: Boolean(projectAuth.migratedFromLegacyApiKey)
230
- };
231
- }
232
-
233
- let legacyProjectApiKeyWarned = false;
234
-
235
- function getLegacyProjectApiKey() {
236
- const projectConfig = readNearestProjectConfig();
237
- if (!projectConfig) {
238
- return null;
239
- }
240
-
241
- const legacyApiKey = projectConfig.config.apiKey;
242
- if (typeof legacyApiKey === 'string' && legacyApiKey.length > 0) {
243
- return legacyApiKey;
244
- }
245
- return null;
246
- }
247
-
248
- function getProjectScopedToken() {
249
- const projectAuth = getProjectScopedSessionState();
250
- if (!projectAuth) {
251
- return null;
252
- }
253
-
254
- const expiresAt = new Date(projectAuth.expiresAt).getTime();
255
- if (!Number.isFinite(expiresAt)) {
256
- return null;
257
- }
258
-
259
- // Consider token expired slightly early to avoid boundary race conditions.
260
- if (Date.now() >= expiresAt - 60_000) {
261
- return null;
262
- }
263
-
264
- return projectAuth.token;
265
- }
266
-
267
- /**
268
- * Get stored API key
269
- * @deprecated API keys are no longer stored on disk. Returns null.
270
- * Use environment variable BOOTSPRING_API_KEY or project-scoped tokens instead.
271
- */
272
- function getStoredApiKey() {
273
- // API keys are no longer stored in credentials for security
274
- // They should only come from env vars or be exchanged for scoped tokens
275
- return null;
276
- }
277
-
278
- /**
279
- * Backwards-compatible alias for legacy .bootspring.json project API key.
280
- */
281
- function getProjectApiKey() {
282
- return getLegacyProjectApiKey();
283
- }
284
-
285
- /**
286
- * Get API key (if using API key auth)
287
- * Checks:
288
- * 1) env var
289
- * 2) short-lived project scoped token in .bootspring.json
290
- * 3) encrypted credential API key
291
- * 4) legacy .bootspring.json apiKey (migration fallback only)
292
- */
293
- function getApiKey() {
294
- // 1. Environment variable (for CI/CD)
295
- const envApiKey = process.env.BOOTSPRING_API_KEY;
296
- if (envApiKey) {
297
- return envApiKey;
298
- }
299
-
300
- // If a valid JWT session exists, prefer session auth and suppress API-key fallback.
301
- if (getToken()) {
302
- return null;
303
- }
304
-
305
- // 2. Project-scoped short-lived token.
306
- const projectScopedToken = getProjectScopedToken();
307
- if (projectScopedToken) {
308
- return projectScopedToken;
309
- }
310
-
311
- // 3. Encrypted credentials file.
312
- const storedApiKey = getStoredApiKey();
313
- if (storedApiKey) {
314
- return storedApiKey;
315
- }
316
-
317
- // 4. Legacy project API key fallback (read-only migration path).
318
- const legacyApiKey = getLegacyProjectApiKey();
319
- if (legacyApiKey) {
320
- if (!legacyProjectApiKeyWarned) {
321
- legacyProjectApiKeyWarned = true;
322
- console.warn(
323
- '[bootspring] Using legacy .bootspring.json apiKey fallback. ' +
324
- 'Run `bootspring auth login --api-key <key>` to migrate to short-lived project tokens.'
325
- );
326
- }
327
- return legacyApiKey;
328
- }
329
-
330
- return null;
331
- }
332
-
333
- /**
334
- * Check if using API key authentication
335
- */
336
- function isApiKeyAuth() {
337
- if (process.env.BOOTSPRING_API_KEY) {
338
- return true;
339
- }
340
-
341
- if (getToken()) {
342
- return false;
343
- }
344
-
345
- return Boolean(
346
- getProjectScopedToken() ||
347
- getStoredApiKey() ||
348
- getLegacyProjectApiKey()
349
- );
350
- }
351
-
352
- /**
353
- * Get refresh token
354
- */
355
- function getRefreshToken() {
356
- const creds = getCredentials();
357
- return creds?.refreshToken || null;
358
- }
359
-
360
- /**
361
- * Check if user is authenticated
362
- */
363
- function isAuthenticated() {
364
- return !!getToken() || !!getApiKey();
365
- }
366
-
367
- /**
368
- * Get user info from stored credentials
369
- */
370
- function getUser() {
371
- const creds = getCredentials();
372
- return creds?.user || null;
373
- }
374
-
375
- /**
376
- * Get user's current tier
377
- * Checks environment variable first, then stored credentials
378
- */
379
- function getTier() {
380
- // Environment variable takes precedence (for CI/CD and sandboxed AI assistants)
381
- const envTier = process.env.BOOTSPRING_USER_TIER;
382
- if (envTier) {
383
- return envTier.toLowerCase();
384
- }
385
-
386
- const user = getUser();
387
- return user?.tier || 'free';
388
- }
389
-
390
- /**
391
- * Save login response (JWT auth)
392
- */
393
- function login(response) {
394
- // Calculate expiration time
395
- const expiresIn = response.expiresIn || '15m';
396
- const expiresMs = parseExpiry(expiresIn);
397
- const expiresAt = new Date(Date.now() + expiresMs).toISOString();
398
-
399
- saveCredentials({
400
- token: response.token,
401
- refreshToken: response.refreshToken,
402
- expiresAt,
403
- user: response.user
404
- });
405
- }
406
-
407
- /**
408
- * Save API key to project's .bootspring.json (for AI assistant sharing)
409
- */
410
- function saveApiKeyToProject(apiKey) {
411
- try {
412
- const projectConfig = readNearestProjectConfig();
413
- if (!projectConfig) {
414
- return false;
415
- }
416
-
417
- const config = { ...projectConfig.config };
418
- config.apiKey = apiKey;
419
- return writeNearestProjectConfig(projectConfig.path, config);
420
- } catch {
421
- // Ignore errors - project config save is best-effort
422
- }
423
- return false;
424
- }
425
-
426
- /**
427
- * Clear API key from project's .bootspring.json in current working tree.
428
- */
429
- function clearProjectApiKey() {
430
- try {
431
- const projectConfig = readNearestProjectConfig();
432
- if (!projectConfig) {
433
- return false;
434
- }
435
-
436
- const config = { ...projectConfig.config };
437
- if (!Object.prototype.hasOwnProperty.call(config, 'apiKey')) {
438
- return false;
439
- }
440
-
441
- delete config.apiKey;
442
- return writeNearestProjectConfig(projectConfig.path, config);
443
- } catch {
444
- // Ignore errors - project config cleanup is best-effort
445
- }
446
- return false;
447
- }
448
-
449
- /**
450
- * Save API key session
451
- * Note: API keys are NOT stored on disk. Only user info is saved.
452
- * The API key should be exchanged for a short-lived scoped token immediately.
453
- * @param {string} apiKey - API key (kept in memory only, not stored)
454
- * @param {object} user - User info to store
455
- */
456
- function loginWithApiKey(apiKey, user) {
457
- // Only save user info, NOT the API key itself
458
- // API key should be exchanged for scoped token via api-client.js
459
- if (user !== undefined) {
460
- const credentials = { user };
461
- saveCredentials(credentials);
462
- }
463
-
464
- // Clear any legacy API keys from project config
465
- clearProjectApiKey();
466
- }
467
-
468
- /**
469
- * Update tokens (after refresh)
470
- */
471
- function updateTokens(response) {
472
- const creds = getCredentials() || {};
473
- const expiresIn = response.expiresIn || '15m';
474
- const expiresMs = parseExpiry(expiresIn);
475
- const expiresAt = new Date(Date.now() + expiresMs).toISOString();
476
-
477
- saveCredentials({
478
- ...creds,
479
- token: response.token,
480
- refreshToken: response.refreshToken,
481
- expiresAt
482
- });
483
- }
484
-
485
- /**
486
- * Parse expiry string (e.g., '15m', '1h', '7d')
487
- */
488
- function parseExpiry(expiry) {
489
- const match = expiry.match(/^(\d+)([mhd])$/);
490
- if (!match) return 15 * 60 * 1000; // Default 15 minutes
491
-
492
- const value = parseInt(match[1]);
493
- const unit = match[2];
494
-
495
- switch (unit) {
496
- case 'm': return value * 60 * 1000;
497
- case 'h': return value * 60 * 60 * 1000;
498
- case 'd': return value * 24 * 60 * 60 * 1000;
499
- default: return 15 * 60 * 1000;
500
- }
501
- }
502
-
503
- function saveProjectScopedSession(token, options = {}) {
504
- try {
505
- const projectConfig = readNearestProjectConfig();
506
- if (!projectConfig || !token) {
507
- return false;
508
- }
509
-
510
- const resolvedExpiresAt = (() => {
511
- if (options.expiresAt) {
512
- return options.expiresAt;
513
- }
514
- if (typeof options.expiresIn === 'number' && Number.isFinite(options.expiresIn)) {
515
- return new Date(Date.now() + options.expiresIn * 1000).toISOString();
516
- }
517
- if (typeof options.expiresIn === 'string') {
518
- return new Date(Date.now() + parseExpiry(options.expiresIn)).toISOString();
519
- }
520
- return new Date(Date.now() + 15 * 60 * 1000).toISOString();
521
- })();
522
-
523
- const nextConfig = {
524
- ...projectConfig.config,
525
- projectAuth: {
526
- token,
527
- expiresAt: resolvedExpiresAt,
528
- issuedAt: new Date().toISOString(),
529
- source: options.source || 'api-key-exchange',
530
- migratedFromLegacyApiKey: Boolean(options.migratedFromLegacyApiKey)
531
- }
532
- };
533
-
534
- if (options.migratedFromLegacyApiKey && Object.prototype.hasOwnProperty.call(nextConfig, 'apiKey')) {
535
- delete nextConfig.apiKey;
536
- }
537
-
538
- return writeNearestProjectConfig(projectConfig.path, nextConfig);
539
- } catch {
540
- // Ignore errors - project scoped session save is best-effort.
541
- }
542
- return false;
543
- }
544
-
545
- function clearProjectScopedSession() {
546
- try {
547
- const projectConfig = readNearestProjectConfig();
548
- if (!projectConfig || !projectConfig.config.projectAuth) {
549
- return false;
550
- }
551
-
552
- const nextConfig = { ...projectConfig.config };
553
- delete nextConfig.projectAuth;
554
- return writeNearestProjectConfig(projectConfig.path, nextConfig);
555
- } catch {
556
- // Ignore errors - project scoped session cleanup is best-effort.
557
- }
558
- return false;
559
- }
560
-
561
- /**
562
- * Logout
563
- */
564
- function logout() {
565
- clearCredentials();
566
- clearProjectScopedSession();
567
- clearProjectApiKey();
568
- }
569
-
570
- /**
571
- * Get global config
572
- */
573
- function getConfig() {
574
- try {
575
- if (fs.existsSync(CONFIG_FILE)) {
576
- return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
577
- }
578
- } catch {
579
- // Ignore
580
- }
581
- return {};
582
- }
583
-
584
- /**
585
- * Save global config
586
- */
587
- function saveConfig(config) {
588
- ensureDir();
589
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
590
- }
591
-
592
- /**
593
- * Get credentials file path (for display)
594
- */
595
- function getCredentialsPath() {
596
- return CREDENTIALS_FILE;
597
- }
598
-
599
- /**
600
- * Generate a stable device fingerprint based on machine characteristics
601
- * This creates a unique identifier for this device that persists across sessions
602
- * @returns {string} Device fingerprint hash
603
- */
604
- function generateDeviceFingerprint() {
605
- const components = [
606
- os.hostname(),
607
- os.userInfo().username,
608
- os.platform(),
609
- os.arch(),
610
- os.cpus()[0]?.model || 'unknown-cpu',
611
- os.homedir(),
612
- // Network interfaces (stable MAC addresses)
613
- Object.values(os.networkInterfaces())
614
- .flat()
615
- .filter(iface => iface && !iface.internal && iface.mac !== '00:00:00:00:00:00')
616
- .map(iface => iface.mac)
617
- .sort()
618
- .join(',')
619
- ];
620
-
621
- return crypto
622
- .createHash('sha256')
623
- .update(components.join('|'))
624
- .digest('hex');
625
- }
626
-
627
- /**
628
- * Get or create persistent device ID
629
- * The device ID is generated once and stored for consistency
630
- * @returns {object} Device info { deviceId, fingerprint, createdAt }
631
- */
632
- function getDeviceInfo() {
633
- ensureDir();
634
-
635
- try {
636
- if (fs.existsSync(DEVICE_FILE)) {
637
- const stored = JSON.parse(fs.readFileSync(DEVICE_FILE, 'utf-8'));
638
- // Verify fingerprint still matches (detect if copied to another machine)
639
- const currentFingerprint = generateDeviceFingerprint();
640
- if (stored.fingerprint === currentFingerprint) {
641
- return stored;
642
- }
643
- // Fingerprint mismatch - device file was copied, regenerate
644
- }
645
- } catch {
646
- // Ignore read errors, regenerate
647
- }
648
-
649
- // Generate new device info
650
- const deviceInfo = {
651
- deviceId: crypto.randomUUID(),
652
- fingerprint: generateDeviceFingerprint(),
653
- createdAt: new Date().toISOString(),
654
- platform: os.platform(),
655
- arch: os.arch(),
656
- hostname: os.hostname()
657
- };
658
-
659
- fs.writeFileSync(DEVICE_FILE, JSON.stringify(deviceInfo, null, 2), { mode: 0o600 });
660
- return deviceInfo;
661
- }
662
-
663
- /**
664
- * Get device ID for authentication requests
665
- * @returns {string} Device ID
666
- */
667
- function getDeviceId() {
668
- return getDeviceInfo().deviceId;
669
- }
670
-
671
- /**
672
- * Get full device context for authentication
673
- * Includes device ID plus current session info for server-side tracking
674
- * @returns {object} Device context for auth requests
675
- */
676
- function getDeviceContext() {
677
- const info = getDeviceInfo();
678
- return {
679
- deviceId: info.deviceId,
680
- platform: info.platform,
681
- arch: info.arch,
682
- hostname: info.hostname,
683
- cliVersion: require('../package.json').version,
684
- nodeVersion: process.version,
685
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
686
- };
687
- }
688
-
689
- /**
690
- * Clear device info (for testing or reset)
691
- */
692
- function clearDeviceInfo() {
693
- try {
694
- if (fs.existsSync(DEVICE_FILE)) {
695
- fs.unlinkSync(DEVICE_FILE);
696
- }
697
- } catch {
698
- // Ignore delete errors
699
- }
700
- }
701
-
702
- module.exports = {
703
- BOOTSPRING_DIR,
704
- ensureDir,
705
- getCredentials,
706
- saveCredentials,
707
- clearCredentials,
708
- getToken,
709
- getTokenExpiryStatus,
710
- getApiKey,
711
- getProjectApiKey,
712
- getProjectScopedToken,
713
- getStoredApiKey,
714
- getLegacyProjectApiKey,
715
- isApiKeyAuth,
716
- getRefreshToken,
717
- isAuthenticated,
718
- getUser,
719
- getTier,
720
- login,
721
- loginWithApiKey,
722
- saveApiKeyToProject,
723
- saveProjectScopedSession,
724
- clearProjectScopedSession,
725
- clearProjectApiKey,
726
- updateTokens,
727
- logout,
728
- getConfig,
729
- saveConfig,
730
- getCredentialsPath,
731
- // Device fingerprinting
732
- generateDeviceFingerprint,
733
- getDeviceInfo,
734
- getDeviceId,
735
- getDeviceContext,
736
- clearDeviceInfo
737
- };