berget 1.0.0 → 1.1.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 +92 -0
- package/dist/index.js +7 -471
- package/dist/src/client.js +193 -102
- package/dist/src/commands/api-keys.js +271 -0
- package/dist/src/commands/auth.js +65 -0
- package/dist/src/commands/autocomplete.js +24 -0
- package/dist/src/commands/billing.js +53 -0
- package/dist/src/commands/chat.js +276 -0
- package/dist/src/commands/clusters.js +69 -0
- package/dist/src/commands/index.js +25 -0
- package/dist/src/commands/models.js +69 -0
- package/dist/src/commands/users.js +43 -0
- package/dist/src/constants/command-structure.js +14 -0
- package/dist/src/services/auth-service.js +49 -47
- package/dist/src/services/chat-service.js +177 -0
- package/dist/src/utils/config-checker.js +50 -0
- package/dist/src/utils/default-api-key.js +111 -0
- package/dist/src/utils/token-manager.js +165 -0
- package/index.ts +5 -566
- package/package.json +6 -1
- package/src/client.ts +262 -80
- package/src/commands/api-keys.ts +364 -0
- package/src/commands/auth.ts +58 -0
- package/src/commands/autocomplete.ts +19 -0
- package/src/commands/billing.ts +41 -0
- package/src/commands/chat.ts +345 -0
- package/src/commands/clusters.ts +65 -0
- package/src/commands/index.ts +23 -0
- package/src/commands/models.ts +63 -0
- package/src/commands/users.ts +37 -0
- package/src/constants/command-structure.ts +16 -0
- package/src/services/auth-service.ts +90 -50
- package/src/services/chat-service.ts +177 -0
- package/src/types/api.d.ts +58 -192
- package/src/utils/config-checker.ts +23 -0
- package/src/utils/default-api-key.ts +94 -0
- package/src/utils/token-manager.ts +150 -0
package/src/types/api.d.ts
CHANGED
|
@@ -351,16 +351,17 @@ export interface paths {
|
|
|
351
351
|
"/v1/auth/login": {
|
|
352
352
|
/**
|
|
353
353
|
* OAuth login
|
|
354
|
-
* @description Initiates OAuth flow
|
|
355
|
-
*
|
|
356
|
-
* If you don't have an account yet, you can create one during the login process
|
|
357
|
-
* by clicking on the "Register" link on the login page.
|
|
358
|
-
*
|
|
359
|
-
* This is the recommended way to authenticate with the Berget AI API.
|
|
354
|
+
* @description Initiates OAuth login flow via Keycloak
|
|
360
355
|
*/
|
|
361
356
|
get: {
|
|
357
|
+
parameters: {
|
|
358
|
+
query?: {
|
|
359
|
+
/** @description URL to redirect to after successful login */
|
|
360
|
+
redirect_uri?: string;
|
|
361
|
+
};
|
|
362
|
+
};
|
|
362
363
|
responses: {
|
|
363
|
-
/** @description Redirects to
|
|
364
|
+
/** @description Redirects to Keycloak for login */
|
|
364
365
|
302: {
|
|
365
366
|
content: never;
|
|
366
367
|
};
|
|
@@ -370,98 +371,60 @@ export interface paths {
|
|
|
370
371
|
"/v1/auth/callback": {
|
|
371
372
|
/**
|
|
372
373
|
* OAuth callback
|
|
373
|
-
* @description Handles
|
|
374
|
+
* @description Handles Keycloak login callback and exchanges token
|
|
374
375
|
*/
|
|
375
376
|
get: {
|
|
376
377
|
parameters: {
|
|
377
|
-
query
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
/** @description State parameter for CSRF protection */
|
|
381
|
-
state?: string;
|
|
378
|
+
query: {
|
|
379
|
+
code: string;
|
|
380
|
+
state: string;
|
|
382
381
|
};
|
|
383
382
|
};
|
|
384
383
|
responses: {
|
|
385
|
-
/** @description Redirects to frontend
|
|
384
|
+
/** @description Redirects to frontend */
|
|
386
385
|
302: {
|
|
387
386
|
content: never;
|
|
388
387
|
};
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
388
|
+
};
|
|
389
|
+
};
|
|
390
|
+
};
|
|
391
|
+
"/v1/auth/device": {
|
|
392
|
+
/** Initiate device authorization flow */
|
|
393
|
+
post: {
|
|
394
|
+
responses: {
|
|
395
|
+
/** @description Device authorization initiated */
|
|
396
|
+
200: {
|
|
395
397
|
content: never;
|
|
396
398
|
};
|
|
397
399
|
};
|
|
398
400
|
};
|
|
399
401
|
};
|
|
400
|
-
"/v1/auth/
|
|
401
|
-
/**
|
|
402
|
-
* Authenticate with OAuth tokens
|
|
403
|
-
* @description Exchange OAuth tokens for our system token
|
|
404
|
-
*/
|
|
402
|
+
"/v1/auth/device/token": {
|
|
403
|
+
/** Poll for device token */
|
|
405
404
|
post: {
|
|
406
405
|
requestBody: {
|
|
407
406
|
content: {
|
|
408
407
|
"application/json": {
|
|
409
|
-
|
|
410
|
-
sub: string;
|
|
411
|
-
/**
|
|
412
|
-
* Format: email
|
|
413
|
-
* @description User's email address
|
|
414
|
-
*/
|
|
415
|
-
email: string;
|
|
416
|
-
/** @description User's full name */
|
|
417
|
-
name?: string;
|
|
418
|
-
/** @description User's preferred username */
|
|
419
|
-
preferred_username?: string;
|
|
420
|
-
/** @description OAuth access token */
|
|
421
|
-
access_token: string;
|
|
422
|
-
/** @description OAuth refresh token */
|
|
423
|
-
refresh_token?: string;
|
|
424
|
-
/** @description OAuth ID token */
|
|
425
|
-
id_token?: string;
|
|
408
|
+
device_code?: string;
|
|
426
409
|
};
|
|
427
410
|
};
|
|
428
411
|
};
|
|
429
412
|
responses: {
|
|
430
|
-
/** @description
|
|
413
|
+
/** @description Token returned or pending status */
|
|
431
414
|
200: {
|
|
432
|
-
content: {
|
|
433
|
-
"application/json": {
|
|
434
|
-
/** @description JWT token for our system */
|
|
435
|
-
token?: string;
|
|
436
|
-
/** @description User information */
|
|
437
|
-
user?: Record<string, never>;
|
|
438
|
-
};
|
|
439
|
-
};
|
|
440
|
-
};
|
|
441
|
-
/** @description Invalid request */
|
|
442
|
-
400: {
|
|
443
|
-
content: never;
|
|
444
|
-
};
|
|
445
|
-
/** @description Authentication failed */
|
|
446
|
-
401: {
|
|
447
415
|
content: never;
|
|
448
416
|
};
|
|
449
417
|
};
|
|
450
418
|
};
|
|
451
419
|
};
|
|
452
420
|
"/v1/auth/register-url": {
|
|
453
|
-
/**
|
|
454
|
-
* Get registration URL
|
|
455
|
-
* @description Returns the URL where users can register a new account.
|
|
456
|
-
* This is the recommended way for new users to create accounts.
|
|
457
|
-
*/
|
|
421
|
+
/** Get Keycloak registration URL */
|
|
458
422
|
get: {
|
|
459
423
|
responses: {
|
|
460
|
-
/** @description Registration URL */
|
|
424
|
+
/** @description Registration URL returned */
|
|
461
425
|
200: {
|
|
462
426
|
content: {
|
|
463
427
|
"application/json": {
|
|
464
|
-
/** @description URL to registration page */
|
|
465
428
|
url?: string;
|
|
466
429
|
};
|
|
467
430
|
};
|
|
@@ -469,82 +432,14 @@ export interface paths {
|
|
|
469
432
|
};
|
|
470
433
|
};
|
|
471
434
|
};
|
|
472
|
-
"/v1/auth/device": {
|
|
473
|
-
/**
|
|
474
|
-
* Initiate device authorization flow
|
|
475
|
-
* @description Initiates a device authorization flow for CLI tools.
|
|
476
|
-
* Returns a verification URL and device code for the user to complete authentication.
|
|
477
|
-
*/
|
|
478
|
-
post: {
|
|
479
|
-
responses: {
|
|
480
|
-
/** @description Device authorization initiated */
|
|
481
|
-
200: {
|
|
482
|
-
content: {
|
|
483
|
-
"application/json": {
|
|
484
|
-
/** @description URL where the user should go to authenticate */
|
|
485
|
-
verification_url?: string;
|
|
486
|
-
/** @description Code the user should enter on the verification page */
|
|
487
|
-
user_code?: string;
|
|
488
|
-
/** @description Code used by the device to poll for authentication status */
|
|
489
|
-
device_code?: string;
|
|
490
|
-
/** @description Seconds until the device code expires */
|
|
491
|
-
expires_in?: number;
|
|
492
|
-
/** @description Polling interval in seconds */
|
|
493
|
-
interval?: number;
|
|
494
|
-
};
|
|
495
|
-
};
|
|
496
|
-
};
|
|
497
|
-
};
|
|
498
|
-
};
|
|
499
|
-
};
|
|
500
|
-
"/v1/auth/device/token": {
|
|
501
|
-
/**
|
|
502
|
-
* Poll for device authorization token
|
|
503
|
-
* @description Polls for the completion of a device authorization flow.
|
|
504
|
-
* The CLI tool should call this endpoint repeatedly until authentication is complete.
|
|
505
|
-
*/
|
|
506
|
-
post: {
|
|
507
|
-
requestBody: {
|
|
508
|
-
content: {
|
|
509
|
-
"application/json": {
|
|
510
|
-
/** @description Device code received from the /device endpoint */
|
|
511
|
-
device_code: string;
|
|
512
|
-
};
|
|
513
|
-
};
|
|
514
|
-
};
|
|
515
|
-
responses: {
|
|
516
|
-
/** @description Authentication successful */
|
|
517
|
-
200: {
|
|
518
|
-
content: {
|
|
519
|
-
"application/json": {
|
|
520
|
-
/** @description JWT token for API access */
|
|
521
|
-
token?: string;
|
|
522
|
-
/** @description User information */
|
|
523
|
-
user?: Record<string, never>;
|
|
524
|
-
};
|
|
525
|
-
};
|
|
526
|
-
};
|
|
527
|
-
/** @description Invalid request */
|
|
528
|
-
400: {
|
|
529
|
-
content: never;
|
|
530
|
-
};
|
|
531
|
-
/** @description Authentication pending or failed */
|
|
532
|
-
401: {
|
|
533
|
-
content: never;
|
|
534
|
-
};
|
|
535
|
-
};
|
|
536
|
-
};
|
|
537
|
-
};
|
|
538
435
|
"/v1/auth/logout": {
|
|
539
436
|
/**
|
|
540
437
|
* Logout
|
|
541
|
-
* @description
|
|
542
|
-
* Optionally accepts a redirect_uri query parameter to specify where to redirect after logout.
|
|
438
|
+
* @description Clears cookies and redirects to Keycloak logout
|
|
543
439
|
*/
|
|
544
440
|
get: {
|
|
545
441
|
parameters: {
|
|
546
442
|
query?: {
|
|
547
|
-
/** @description URL to redirect to after logout */
|
|
548
443
|
redirect_uri?: string;
|
|
549
444
|
};
|
|
550
445
|
};
|
|
@@ -969,6 +864,28 @@ export interface paths {
|
|
|
969
864
|
};
|
|
970
865
|
};
|
|
971
866
|
};
|
|
867
|
+
"/v1/users/me": {
|
|
868
|
+
/**
|
|
869
|
+
* Get current user profile
|
|
870
|
+
* @description Retrieves the profile of the currently authenticated user
|
|
871
|
+
*/
|
|
872
|
+
get: {
|
|
873
|
+
responses: {
|
|
874
|
+
/** @description User profile */
|
|
875
|
+
200: {
|
|
876
|
+
content: {
|
|
877
|
+
"application/json": components["schemas"]["UserProfile"];
|
|
878
|
+
};
|
|
879
|
+
};
|
|
880
|
+
/** @description Unauthorized */
|
|
881
|
+
401: {
|
|
882
|
+
content: {
|
|
883
|
+
"application/json": components["schemas"]["ErrorResponse"];
|
|
884
|
+
};
|
|
885
|
+
};
|
|
886
|
+
};
|
|
887
|
+
};
|
|
888
|
+
};
|
|
972
889
|
"/v1/users/{id}": {
|
|
973
890
|
/**
|
|
974
891
|
* Get user details
|
|
@@ -1073,28 +990,6 @@ export interface paths {
|
|
|
1073
990
|
};
|
|
1074
991
|
};
|
|
1075
992
|
};
|
|
1076
|
-
"/v1/users/me": {
|
|
1077
|
-
/**
|
|
1078
|
-
* Get current user profile
|
|
1079
|
-
* @description Retrieves the profile of the currently authenticated user
|
|
1080
|
-
*/
|
|
1081
|
-
get: {
|
|
1082
|
-
responses: {
|
|
1083
|
-
/** @description User profile */
|
|
1084
|
-
200: {
|
|
1085
|
-
content: {
|
|
1086
|
-
"application/json": components["schemas"]["UserProfile"];
|
|
1087
|
-
};
|
|
1088
|
-
};
|
|
1089
|
-
/** @description Unauthorized */
|
|
1090
|
-
401: {
|
|
1091
|
-
content: {
|
|
1092
|
-
"application/json": components["schemas"]["ErrorResponse"];
|
|
1093
|
-
};
|
|
1094
|
-
};
|
|
1095
|
-
};
|
|
1096
|
-
};
|
|
1097
|
-
};
|
|
1098
993
|
"/v1/users/invite": {
|
|
1099
994
|
/**
|
|
1100
995
|
* Invite team member
|
|
@@ -1797,46 +1692,17 @@ export interface components {
|
|
|
1797
1692
|
};
|
|
1798
1693
|
};
|
|
1799
1694
|
};
|
|
1800
|
-
/** @description Pricing information for the model */
|
|
1801
|
-
ModelPricing: {
|
|
1802
|
-
/** @description Cost per token for input in the specified currency */
|
|
1803
|
-
input: number;
|
|
1804
|
-
/** @description Cost per token for output in the specified currency */
|
|
1805
|
-
output: number;
|
|
1806
|
-
/** @description The unit of pricing (e.g., "token") */
|
|
1807
|
-
unit: string;
|
|
1808
|
-
/** @description The currency of the pricing (e.g., "USD") */
|
|
1809
|
-
currency: string;
|
|
1810
|
-
};
|
|
1811
|
-
/** @description Model capabilities */
|
|
1812
|
-
ModelCapabilities: {
|
|
1813
|
-
/** @description Whether the model supports vision/image input */
|
|
1814
|
-
vision: boolean;
|
|
1815
|
-
/** @description Whether the model supports function calling */
|
|
1816
|
-
function_calling: boolean;
|
|
1817
|
-
/** @description Whether the model supports JSON mode */
|
|
1818
|
-
json_mode: boolean;
|
|
1819
|
-
};
|
|
1820
1695
|
Model: {
|
|
1821
1696
|
/** @description Unique identifier for the model */
|
|
1822
1697
|
id: string;
|
|
1823
|
-
/**
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
created: number;
|
|
1830
|
-
/** @description Organization that owns the model */
|
|
1831
|
-
owned_by: string;
|
|
1832
|
-
pricing: components["schemas"]["ModelPricing"];
|
|
1833
|
-
capabilities: components["schemas"]["ModelCapabilities"];
|
|
1834
|
-
};
|
|
1835
|
-
ModelList: {
|
|
1836
|
-
data: components["schemas"]["Model"][];
|
|
1837
|
-
/** @enum {string} */
|
|
1838
|
-
object: "list";
|
|
1698
|
+
/** @description Name of the model */
|
|
1699
|
+
name: string;
|
|
1700
|
+
/** @description Description of the model */
|
|
1701
|
+
description: string;
|
|
1702
|
+
/** @description Whether the model is active */
|
|
1703
|
+
active: boolean;
|
|
1839
1704
|
};
|
|
1705
|
+
ModelList: components["schemas"]["Model"][];
|
|
1840
1706
|
/** @description Cost information for this usage */
|
|
1841
1707
|
TokenCost: {
|
|
1842
1708
|
/** @description Cost amount */
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Check for .bergetconfig file and handle cluster switching
|
|
6
|
+
*/
|
|
7
|
+
export function checkBergetConfig(): void {
|
|
8
|
+
const configPath = path.join(process.cwd(), '.bergetconfig')
|
|
9
|
+
if (fs.existsSync(configPath)) {
|
|
10
|
+
try {
|
|
11
|
+
const config = fs.readFileSync(configPath, 'utf8')
|
|
12
|
+
const match = config.match(/cluster:\s*(.+)/)
|
|
13
|
+
if (match && match[1]) {
|
|
14
|
+
const clusterName = match[1].trim()
|
|
15
|
+
console.log(`🔄 Berget: Switched to cluster "${clusterName}"`)
|
|
16
|
+
console.log('✓ kubectl config updated')
|
|
17
|
+
console.log('')
|
|
18
|
+
}
|
|
19
|
+
} catch (error) {
|
|
20
|
+
// Silently ignore errors reading config
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
import * as os from 'os'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
|
|
6
|
+
interface DefaultApiKeyData {
|
|
7
|
+
id: string
|
|
8
|
+
name: string
|
|
9
|
+
prefix: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Manages the default API key for chat commands
|
|
14
|
+
*/
|
|
15
|
+
export class DefaultApiKeyManager {
|
|
16
|
+
private static instance: DefaultApiKeyManager
|
|
17
|
+
private configFilePath: string
|
|
18
|
+
private defaultApiKey: DefaultApiKeyData | null = null
|
|
19
|
+
|
|
20
|
+
private constructor() {
|
|
21
|
+
// Set up config file path in user's home directory
|
|
22
|
+
const bergetDir = path.join(os.homedir(), '.berget')
|
|
23
|
+
if (!fs.existsSync(bergetDir)) {
|
|
24
|
+
fs.mkdirSync(bergetDir, { recursive: true })
|
|
25
|
+
}
|
|
26
|
+
this.configFilePath = path.join(bergetDir, 'default-api-key.json')
|
|
27
|
+
this.loadConfig()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public static getInstance(): DefaultApiKeyManager {
|
|
31
|
+
if (!DefaultApiKeyManager.instance) {
|
|
32
|
+
DefaultApiKeyManager.instance = new DefaultApiKeyManager()
|
|
33
|
+
}
|
|
34
|
+
return DefaultApiKeyManager.instance
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load default API key from file
|
|
39
|
+
*/
|
|
40
|
+
private loadConfig(): void {
|
|
41
|
+
try {
|
|
42
|
+
if (fs.existsSync(this.configFilePath)) {
|
|
43
|
+
const data = fs.readFileSync(this.configFilePath, 'utf8')
|
|
44
|
+
this.defaultApiKey = JSON.parse(data)
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error(chalk.dim('Failed to load default API key configuration'))
|
|
48
|
+
this.defaultApiKey = null
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Save default API key to file
|
|
54
|
+
*/
|
|
55
|
+
private saveConfig(): void {
|
|
56
|
+
try {
|
|
57
|
+
if (this.defaultApiKey) {
|
|
58
|
+
fs.writeFileSync(this.configFilePath, JSON.stringify(this.defaultApiKey, null, 2))
|
|
59
|
+
// Set file permissions to be readable only by the owner
|
|
60
|
+
fs.chmodSync(this.configFilePath, 0o600)
|
|
61
|
+
} else {
|
|
62
|
+
// If default API key is null, remove the file
|
|
63
|
+
if (fs.existsSync(this.configFilePath)) {
|
|
64
|
+
fs.unlinkSync(this.configFilePath)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(chalk.dim('Failed to save default API key configuration'))
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Set the default API key
|
|
74
|
+
*/
|
|
75
|
+
public setDefaultApiKey(id: string, name: string, prefix: string): void {
|
|
76
|
+
this.defaultApiKey = { id, name, prefix }
|
|
77
|
+
this.saveConfig()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get the default API key
|
|
82
|
+
*/
|
|
83
|
+
public getDefaultApiKey(): DefaultApiKeyData | null {
|
|
84
|
+
return this.defaultApiKey
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Clear the default API key
|
|
89
|
+
*/
|
|
90
|
+
public clearDefaultApiKey(): void {
|
|
91
|
+
this.defaultApiKey = null
|
|
92
|
+
this.saveConfig()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
import * as os from 'os'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
|
|
6
|
+
interface TokenData {
|
|
7
|
+
access_token: string
|
|
8
|
+
refresh_token: string
|
|
9
|
+
expires_at: number // timestamp in milliseconds
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Manages authentication tokens including refresh functionality
|
|
14
|
+
*/
|
|
15
|
+
export class TokenManager {
|
|
16
|
+
private static instance: TokenManager
|
|
17
|
+
private tokenFilePath: string
|
|
18
|
+
private tokenData: TokenData | null = null
|
|
19
|
+
|
|
20
|
+
private constructor() {
|
|
21
|
+
// Set up token file path in user's home directory
|
|
22
|
+
const bergetDir = path.join(os.homedir(), '.berget')
|
|
23
|
+
if (!fs.existsSync(bergetDir)) {
|
|
24
|
+
fs.mkdirSync(bergetDir, { recursive: true })
|
|
25
|
+
}
|
|
26
|
+
this.tokenFilePath = path.join(bergetDir, 'auth.json')
|
|
27
|
+
this.loadToken()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public static getInstance(): TokenManager {
|
|
31
|
+
if (!TokenManager.instance) {
|
|
32
|
+
TokenManager.instance = new TokenManager()
|
|
33
|
+
}
|
|
34
|
+
return TokenManager.instance
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load token data from file
|
|
39
|
+
*/
|
|
40
|
+
private loadToken(): void {
|
|
41
|
+
try {
|
|
42
|
+
if (fs.existsSync(this.tokenFilePath)) {
|
|
43
|
+
const data = fs.readFileSync(this.tokenFilePath, 'utf8')
|
|
44
|
+
this.tokenData = JSON.parse(data)
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error(chalk.dim('Failed to load authentication token'))
|
|
48
|
+
this.tokenData = null
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Save token data to file
|
|
54
|
+
*/
|
|
55
|
+
private saveToken(): void {
|
|
56
|
+
try {
|
|
57
|
+
if (this.tokenData) {
|
|
58
|
+
fs.writeFileSync(this.tokenFilePath, JSON.stringify(this.tokenData, null, 2))
|
|
59
|
+
// Set file permissions to be readable only by the owner
|
|
60
|
+
fs.chmodSync(this.tokenFilePath, 0o600)
|
|
61
|
+
} else {
|
|
62
|
+
// If token data is null, remove the file
|
|
63
|
+
if (fs.existsSync(this.tokenFilePath)) {
|
|
64
|
+
fs.unlinkSync(this.tokenFilePath)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(chalk.dim('Failed to save authentication token'))
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get the current access token
|
|
74
|
+
* @returns The access token or null if not available
|
|
75
|
+
*/
|
|
76
|
+
public getAccessToken(): string | null {
|
|
77
|
+
if (!this.tokenData) return null
|
|
78
|
+
return this.tokenData.access_token
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get the refresh token
|
|
83
|
+
* @returns The refresh token or null if not available
|
|
84
|
+
*/
|
|
85
|
+
public getRefreshToken(): string | null {
|
|
86
|
+
if (!this.tokenData) return null
|
|
87
|
+
return this.tokenData.refresh_token
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if the access token is expired
|
|
92
|
+
* @returns true if expired or about to expire (within 5 minutes), false otherwise
|
|
93
|
+
*/
|
|
94
|
+
public isTokenExpired(): boolean {
|
|
95
|
+
if (!this.tokenData || !this.tokenData.expires_at) return true
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// Consider token expired if it's within 10 minutes of expiration
|
|
99
|
+
// Using a larger buffer to be more proactive about refreshing
|
|
100
|
+
const expirationBuffer = 10 * 60 * 1000 // 10 minutes in milliseconds
|
|
101
|
+
const isExpired = Date.now() + expirationBuffer >= this.tokenData.expires_at;
|
|
102
|
+
|
|
103
|
+
if (isExpired && process.argv.includes('--debug')) {
|
|
104
|
+
console.log(chalk.yellow(`DEBUG: Token expired or expiring soon. Current time: ${new Date().toISOString()}, Expiry: ${new Date(this.tokenData.expires_at).toISOString()}`));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return isExpired;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
// If there's any error checking expiration, assume token is expired
|
|
110
|
+
console.error(chalk.dim(`Error checking token expiration: ${error instanceof Error ? error.message : String(error)}`));
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Set new token data
|
|
117
|
+
* @param accessToken The new access token
|
|
118
|
+
* @param refreshToken The new refresh token
|
|
119
|
+
* @param expiresIn Expiration time in seconds
|
|
120
|
+
*/
|
|
121
|
+
public setTokens(accessToken: string, refreshToken: string, expiresIn: number): void {
|
|
122
|
+
this.tokenData = {
|
|
123
|
+
access_token: accessToken,
|
|
124
|
+
refresh_token: refreshToken,
|
|
125
|
+
expires_at: Date.now() + (expiresIn * 1000)
|
|
126
|
+
}
|
|
127
|
+
this.saveToken()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Update just the access token and its expiration
|
|
132
|
+
* @param accessToken The new access token
|
|
133
|
+
* @param expiresIn Expiration time in seconds
|
|
134
|
+
*/
|
|
135
|
+
public updateAccessToken(accessToken: string, expiresIn: number): void {
|
|
136
|
+
if (!this.tokenData) return
|
|
137
|
+
|
|
138
|
+
this.tokenData.access_token = accessToken
|
|
139
|
+
this.tokenData.expires_at = Date.now() + (expiresIn * 1000)
|
|
140
|
+
this.saveToken()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Clear all token data
|
|
145
|
+
*/
|
|
146
|
+
public clearTokens(): void {
|
|
147
|
+
this.tokenData = null
|
|
148
|
+
this.saveToken()
|
|
149
|
+
}
|
|
150
|
+
}
|