berget 2.1.1 → 2.2.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/dist/index.js +1 -0
- package/dist/package.json +1 -1
- package/dist/src/client.js +22 -6
- package/dist/src/commands/code.js +33 -4
- package/dist/src/services/api-key-service.js +7 -4
- package/dist/src/services/auth-service.js +280 -110
- package/dist/src/utils/token-manager.js +11 -6
- package/index.ts +1 -0
- package/opencode.json +5 -1
- package/package.json +1 -1
- package/src/client.ts +29 -9
- package/src/commands/code.ts +32 -5
- package/src/services/api-key-service.ts +6 -0
- package/src/services/auth-service.ts +319 -184
- package/src/utils/token-manager.ts +13 -7
- package/dist/src/schemas/opencode-schema.json +0 -1121
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ commander_1.program
|
|
|
25
25
|
Version: ${package_json_1.version}`)
|
|
26
26
|
.version(package_json_1.version, '-v, --version')
|
|
27
27
|
.option('--local', 'Use local API endpoint (hidden)', false)
|
|
28
|
+
.option('--stage', 'Use stage API endpoint', false)
|
|
28
29
|
.option('--debug', 'Enable debug output', false);
|
|
29
30
|
// Register all commands
|
|
30
31
|
(0, commands_1.registerCommands)(commander_1.program);
|
package/dist/package.json
CHANGED
package/dist/src/client.js
CHANGED
|
@@ -19,12 +19,19 @@ const token_manager_1 = require("./utils/token-manager");
|
|
|
19
19
|
const logger_1 = require("./utils/logger");
|
|
20
20
|
// API Base URL
|
|
21
21
|
// Use --local flag to test against local API
|
|
22
|
+
// Use --stage flag to test against stage API
|
|
22
23
|
const isLocalMode = process.argv.includes('--local');
|
|
24
|
+
const isStageMode = process.argv.includes('--stage');
|
|
23
25
|
exports.API_BASE_URL = process.env.BERGET_API_URL ||
|
|
24
|
-
(isLocalMode ? 'http://localhost:3000' :
|
|
26
|
+
(isLocalMode ? 'http://localhost:3000' :
|
|
27
|
+
isStageMode ? 'https://api.stage.berget.ai' :
|
|
28
|
+
'https://api.berget.ai'); // production default
|
|
25
29
|
if (isLocalMode && !process.env.BERGET_API_URL) {
|
|
26
30
|
logger_1.logger.debug('Using local API endpoint: http://localhost:3000');
|
|
27
31
|
}
|
|
32
|
+
else if (isStageMode && !process.env.BERGET_API_URL) {
|
|
33
|
+
logger_1.logger.debug('Using stage API endpoint: https://api.stage.berget.ai');
|
|
34
|
+
}
|
|
28
35
|
// Create a typed client for the Berget API
|
|
29
36
|
exports.apiClient = (0, openapi_fetch_1.default)({
|
|
30
37
|
baseUrl: exports.API_BASE_URL,
|
|
@@ -177,6 +184,12 @@ const createAuthenticatedClient = () => {
|
|
|
177
184
|
});
|
|
178
185
|
};
|
|
179
186
|
exports.createAuthenticatedClient = createAuthenticatedClient;
|
|
187
|
+
// Keycloak configuration for token refresh (must match auth-service.ts)
|
|
188
|
+
const KEYCLOAK_URL = (isStageMode || isLocalMode)
|
|
189
|
+
? 'https://keycloak.stage.berget.ai'
|
|
190
|
+
: 'https://keycloak.berget.ai';
|
|
191
|
+
const KEYCLOAK_REALM = 'berget';
|
|
192
|
+
const KEYCLOAK_CLIENT_ID = 'berget-code';
|
|
180
193
|
// Helper function to refresh the access token
|
|
181
194
|
function refreshAccessToken(tokenManager) {
|
|
182
195
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -185,15 +198,18 @@ function refreshAccessToken(tokenManager) {
|
|
|
185
198
|
if (!refreshToken)
|
|
186
199
|
return false;
|
|
187
200
|
logger_1.logger.debug('Attempting to refresh access token');
|
|
188
|
-
//
|
|
201
|
+
// Refresh directly against Keycloak (berget-code is a public PKCE client)
|
|
189
202
|
try {
|
|
190
|
-
const response = yield fetch(`${
|
|
203
|
+
const response = yield fetch(`${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token`, {
|
|
191
204
|
method: 'POST',
|
|
192
205
|
headers: {
|
|
193
|
-
'Content-Type': 'application/
|
|
194
|
-
Accept: 'application/json',
|
|
206
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
195
207
|
},
|
|
196
|
-
body:
|
|
208
|
+
body: new URLSearchParams({
|
|
209
|
+
grant_type: 'refresh_token',
|
|
210
|
+
client_id: KEYCLOAK_CLIENT_ID,
|
|
211
|
+
refresh_token: refreshToken,
|
|
212
|
+
}),
|
|
197
213
|
});
|
|
198
214
|
// Handle HTTP errors
|
|
199
215
|
if (!response.ok) {
|
|
@@ -711,18 +711,19 @@ function registerCodeCommands(program) {
|
|
|
711
711
|
npm: '@ai-sdk/openai-compatible',
|
|
712
712
|
name: 'Berget AI',
|
|
713
713
|
options: {
|
|
714
|
-
baseURL: '
|
|
714
|
+
baseURL: '{env:BERGET_API_URL}',
|
|
715
715
|
apiKey: '{env:BERGET_API_KEY}',
|
|
716
716
|
},
|
|
717
717
|
models: {
|
|
718
718
|
'glm-4.7': {
|
|
719
719
|
name: 'GLM-4.7',
|
|
720
|
-
limit: { output: 4000, context:
|
|
720
|
+
limit: { output: 4000, context: 200000 },
|
|
721
|
+
modalities: { input: ['text'], output: ['text'] },
|
|
721
722
|
},
|
|
722
723
|
'gpt-oss': {
|
|
723
724
|
name: 'GPT-OSS',
|
|
724
725
|
limit: { output: 4000, context: 128000 },
|
|
725
|
-
modalities: ['text', 'image'],
|
|
726
|
+
modalities: { input: ['text', 'image'], output: ['text'] },
|
|
726
727
|
},
|
|
727
728
|
'llama-8b': {
|
|
728
729
|
name: 'llama-3.1-8b',
|
|
@@ -1002,6 +1003,34 @@ All agents follow these principles:
|
|
|
1002
1003
|
}
|
|
1003
1004
|
// Set environment variables for opencode
|
|
1004
1005
|
const env = Object.assign({}, process.env);
|
|
1006
|
+
// Set API base URL based on flags (default to production)
|
|
1007
|
+
const isLocalMode = process.argv.includes('--local');
|
|
1008
|
+
const isStageMode = process.argv.includes('--stage');
|
|
1009
|
+
if (isLocalMode) {
|
|
1010
|
+
env.BERGET_API_URL = 'http://localhost:3000/v1';
|
|
1011
|
+
console.log(chalk_1.default.dim('Using local API: http://localhost:3000/v1'));
|
|
1012
|
+
}
|
|
1013
|
+
else if (isStageMode) {
|
|
1014
|
+
env.BERGET_API_URL = 'https://api.stage.berget.ai/v1';
|
|
1015
|
+
console.log(chalk_1.default.dim('Using stage API: https://api.stage.berget.ai/v1'));
|
|
1016
|
+
}
|
|
1017
|
+
else {
|
|
1018
|
+
env.BERGET_API_URL = 'https://api.berget.ai/v1';
|
|
1019
|
+
}
|
|
1020
|
+
// Auth resolution: JWT first (if valid), then API-key
|
|
1021
|
+
// This ensures seat-based users get proper tracking
|
|
1022
|
+
const jwtToken = (0, client_1.getAuthToken)();
|
|
1023
|
+
if (jwtToken) {
|
|
1024
|
+
env.BERGET_API_KEY = jwtToken;
|
|
1025
|
+
console.log(chalk_1.default.dim('Using JWT token for authentication'));
|
|
1026
|
+
}
|
|
1027
|
+
else if (env.BERGET_API_KEY) {
|
|
1028
|
+
console.log(chalk_1.default.dim('Using API key for authentication'));
|
|
1029
|
+
}
|
|
1030
|
+
else {
|
|
1031
|
+
console.log(chalk_1.default.yellow('Warning: No authentication found'));
|
|
1032
|
+
console.log(chalk_1.default.dim(' Run `berget auth login` or set BERGET_API_KEY'));
|
|
1033
|
+
}
|
|
1005
1034
|
// Prepare opencode command
|
|
1006
1035
|
const opencodeArgs = [];
|
|
1007
1036
|
if (prompt) {
|
|
@@ -1251,7 +1280,7 @@ All agents follow these principles:
|
|
|
1251
1280
|
npm: '@ai-sdk/openai-compatible',
|
|
1252
1281
|
name: 'Berget AI',
|
|
1253
1282
|
options: {
|
|
1254
|
-
baseURL: '
|
|
1283
|
+
baseURL: '{env:BERGET_API_URL}',
|
|
1255
1284
|
apiKey: '{env:BERGET_API_KEY}',
|
|
1256
1285
|
},
|
|
1257
1286
|
models: (0, config_loader_1.getProviderModels)(),
|
|
@@ -50,7 +50,7 @@ class ApiKeyService {
|
|
|
50
50
|
* Command: berget api-keys create
|
|
51
51
|
*/
|
|
52
52
|
create(options) {
|
|
53
|
-
var _a, _b, _c, _d;
|
|
53
|
+
var _a, _b, _c, _d, _e;
|
|
54
54
|
return __awaiter(this, void 0, void 0, function* () {
|
|
55
55
|
try {
|
|
56
56
|
// Validate input before sending request
|
|
@@ -89,13 +89,16 @@ class ApiKeyService {
|
|
|
89
89
|
detailedMessage += '5. Contact support if the problem persists';
|
|
90
90
|
throw new Error(detailedMessage);
|
|
91
91
|
}
|
|
92
|
-
if (((_b = errorObj.error) === null || _b === void 0 ? void 0 : _b.code) === '
|
|
92
|
+
if (((_b = errorObj.error) === null || _b === void 0 ? void 0 : _b.code) === 'USER_NOT_FOUND') {
|
|
93
|
+
throw new Error('Your account is still being set up. Please wait a moment and try again.\n\nIf this issue persists, please contact support at support@berget.ai');
|
|
94
|
+
}
|
|
95
|
+
if (((_c = errorObj.error) === null || _c === void 0 ? void 0 : _c.code) === 'QUOTA_EXCEEDED') {
|
|
93
96
|
throw new Error('You have reached your API key limit. Please delete existing keys or contact support to increase your quota.');
|
|
94
97
|
}
|
|
95
|
-
if (((
|
|
98
|
+
if (((_d = errorObj.error) === null || _d === void 0 ? void 0 : _d.code) === 'INSUFFICIENT_PERMISSIONS') {
|
|
96
99
|
throw new Error('Your account does not have permission to create API keys. Please contact your administrator.');
|
|
97
100
|
}
|
|
98
|
-
if (((
|
|
101
|
+
if (((_e = errorObj.error) === null || _e === void 0 ? void 0 : _e.code) === 'BILLING_REQUIRED') {
|
|
99
102
|
throw new Error('A valid billing method is required to create API keys. Please add a payment method.');
|
|
100
103
|
}
|
|
101
104
|
}
|
|
@@ -41,6 +41,30 @@ const client_1 = require("../client");
|
|
|
41
41
|
const chalk_1 = __importDefault(require("chalk"));
|
|
42
42
|
const error_handler_1 = require("../utils/error-handler");
|
|
43
43
|
const command_structure_1 = require("../constants/command-structure");
|
|
44
|
+
const http = __importStar(require("http"));
|
|
45
|
+
const crypto = __importStar(require("crypto"));
|
|
46
|
+
const url = __importStar(require("url"));
|
|
47
|
+
// Keycloak configuration based on environment
|
|
48
|
+
const isStageMode = process.argv.includes('--stage');
|
|
49
|
+
const isLocalMode = process.argv.includes('--local');
|
|
50
|
+
const KEYCLOAK_URL = (isStageMode || isLocalMode)
|
|
51
|
+
? 'https://keycloak.stage.berget.ai'
|
|
52
|
+
: 'https://keycloak.berget.ai';
|
|
53
|
+
const KEYCLOAK_REALM = 'berget';
|
|
54
|
+
const KEYCLOAK_CLIENT_ID = 'berget-code';
|
|
55
|
+
const CALLBACK_PORT = 8787;
|
|
56
|
+
/**
|
|
57
|
+
* Generate a random string for PKCE code_verifier
|
|
58
|
+
*/
|
|
59
|
+
function generateCodeVerifier() {
|
|
60
|
+
return crypto.randomBytes(32).toString('base64url');
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Generate code_challenge from code_verifier using S256 method
|
|
64
|
+
*/
|
|
65
|
+
function generateCodeChallenge(verifier) {
|
|
66
|
+
return crypto.createHash('sha256').update(verifier).digest('base64url');
|
|
67
|
+
}
|
|
44
68
|
/**
|
|
45
69
|
* Service for authentication operations
|
|
46
70
|
* Command group: auth
|
|
@@ -58,7 +82,9 @@ class AuthService {
|
|
|
58
82
|
whoami() {
|
|
59
83
|
return __awaiter(this, void 0, void 0, function* () {
|
|
60
84
|
try {
|
|
61
|
-
|
|
85
|
+
// Create fresh client to ensure we have the latest token
|
|
86
|
+
const client = (0, client_1.createAuthenticatedClient)();
|
|
87
|
+
const { data: profile, error } = yield client.GET('/v1/users/me');
|
|
62
88
|
if (error) {
|
|
63
89
|
return null;
|
|
64
90
|
}
|
|
@@ -75,123 +101,267 @@ class AuthService {
|
|
|
75
101
|
// Clear any existing token to ensure a fresh login
|
|
76
102
|
(0, client_1.clearAuthToken)();
|
|
77
103
|
console.log(chalk_1.default.blue('Initiating login process...'));
|
|
78
|
-
//
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
104
|
+
// Generate PKCE code verifier and challenge
|
|
105
|
+
const codeVerifier = generateCodeVerifier();
|
|
106
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
107
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
108
|
+
const redirectUri = `http://localhost:${CALLBACK_PORT}/callback`;
|
|
109
|
+
// Build authorization URL
|
|
110
|
+
const authUrl = new URL(`${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/auth`);
|
|
111
|
+
authUrl.searchParams.set('client_id', KEYCLOAK_CLIENT_ID);
|
|
112
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
113
|
+
authUrl.searchParams.set('redirect_uri', redirectUri);
|
|
114
|
+
authUrl.searchParams.set('scope', 'openid email profile');
|
|
115
|
+
authUrl.searchParams.set('state', state);
|
|
116
|
+
authUrl.searchParams.set('code_challenge', codeChallenge);
|
|
117
|
+
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
118
|
+
// Create a promise that resolves when we receive the callback
|
|
119
|
+
const authResult = yield new Promise((resolve) => {
|
|
120
|
+
const server = http.createServer((req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
121
|
+
const parsedUrl = url.parse(req.url || '', true);
|
|
122
|
+
if (parsedUrl.pathname === '/callback') {
|
|
123
|
+
const receivedState = parsedUrl.query.state;
|
|
124
|
+
const code = parsedUrl.query.code;
|
|
125
|
+
const error = parsedUrl.query.error;
|
|
126
|
+
const errorPage = (title, message) => `
|
|
127
|
+
<!DOCTYPE html>
|
|
128
|
+
<html lang="en">
|
|
129
|
+
<head>
|
|
130
|
+
<meta charset="UTF-8">
|
|
131
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
132
|
+
<title>Berget - Authentication Failed</title>
|
|
133
|
+
<style>
|
|
134
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
135
|
+
body {
|
|
136
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
137
|
+
display: flex;
|
|
138
|
+
justify-content: center;
|
|
139
|
+
align-items: center;
|
|
140
|
+
min-height: 100vh;
|
|
141
|
+
background: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 50%, #16213e 100%);
|
|
142
|
+
color: #fff;
|
|
100
143
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
144
|
+
.container {
|
|
145
|
+
text-align: center;
|
|
146
|
+
padding: 3rem;
|
|
147
|
+
max-width: 400px;
|
|
148
|
+
}
|
|
149
|
+
.icon {
|
|
150
|
+
width: 80px;
|
|
151
|
+
height: 80px;
|
|
152
|
+
background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
|
|
153
|
+
border-radius: 50%;
|
|
154
|
+
display: flex;
|
|
155
|
+
align-items: center;
|
|
156
|
+
justify-content: center;
|
|
157
|
+
margin: 0 auto 1.5rem;
|
|
158
|
+
box-shadow: 0 4px 20px rgba(248, 113, 113, 0.3);
|
|
159
|
+
}
|
|
160
|
+
.icon svg {
|
|
161
|
+
width: 40px;
|
|
162
|
+
height: 40px;
|
|
163
|
+
stroke: #fff;
|
|
164
|
+
stroke-width: 3;
|
|
165
|
+
}
|
|
166
|
+
h1 {
|
|
167
|
+
font-size: 1.5rem;
|
|
168
|
+
font-weight: 600;
|
|
169
|
+
margin-bottom: 0.75rem;
|
|
170
|
+
color: #fff;
|
|
171
|
+
}
|
|
172
|
+
p {
|
|
173
|
+
color: #94a3b8;
|
|
174
|
+
font-size: 0.95rem;
|
|
175
|
+
line-height: 1.5;
|
|
176
|
+
}
|
|
177
|
+
.brand {
|
|
178
|
+
margin-top: 2rem;
|
|
179
|
+
opacity: 0.5;
|
|
180
|
+
font-size: 0.8rem;
|
|
181
|
+
letter-spacing: 0.05em;
|
|
182
|
+
}
|
|
183
|
+
</style>
|
|
184
|
+
</head>
|
|
185
|
+
<body>
|
|
186
|
+
<div class="container">
|
|
187
|
+
<div class="icon">
|
|
188
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
|
189
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
190
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
191
|
+
</svg>
|
|
192
|
+
</div>
|
|
193
|
+
<h1>${title}</h1>
|
|
194
|
+
<p>${message}</p>
|
|
195
|
+
<div class="brand">BERGET</div>
|
|
196
|
+
</div>
|
|
197
|
+
</body>
|
|
198
|
+
</html>
|
|
199
|
+
`;
|
|
200
|
+
if (error) {
|
|
201
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
202
|
+
res.end(errorPage('Authentication Failed', String(parsedUrl.query.error_description || error)));
|
|
203
|
+
server.close();
|
|
204
|
+
resolve({ success: false, error });
|
|
205
|
+
return;
|
|
152
206
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
207
|
+
if (receivedState !== state) {
|
|
208
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
209
|
+
res.end(errorPage('Authentication Failed', 'Invalid state parameter. Please try again.'));
|
|
210
|
+
server.close();
|
|
211
|
+
resolve({ success: false, error: 'Invalid state parameter' });
|
|
212
|
+
return;
|
|
156
213
|
}
|
|
157
|
-
|
|
214
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
215
|
+
res.end(`
|
|
216
|
+
<!DOCTYPE html>
|
|
217
|
+
<html lang="en">
|
|
218
|
+
<head>
|
|
219
|
+
<meta charset="UTF-8">
|
|
220
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
221
|
+
<title>Berget - Authentication Successful</title>
|
|
222
|
+
<style>
|
|
223
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
224
|
+
body {
|
|
225
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
226
|
+
display: flex;
|
|
227
|
+
justify-content: center;
|
|
228
|
+
align-items: center;
|
|
229
|
+
min-height: 100vh;
|
|
230
|
+
background: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 50%, #16213e 100%);
|
|
231
|
+
color: #fff;
|
|
232
|
+
}
|
|
233
|
+
.container {
|
|
234
|
+
text-align: center;
|
|
235
|
+
padding: 3rem;
|
|
236
|
+
max-width: 400px;
|
|
237
|
+
}
|
|
238
|
+
.icon {
|
|
239
|
+
width: 80px;
|
|
240
|
+
height: 80px;
|
|
241
|
+
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
|
|
242
|
+
border-radius: 50%;
|
|
243
|
+
display: flex;
|
|
244
|
+
align-items: center;
|
|
245
|
+
justify-content: center;
|
|
246
|
+
margin: 0 auto 1.5rem;
|
|
247
|
+
box-shadow: 0 4px 20px rgba(74, 222, 128, 0.3);
|
|
248
|
+
}
|
|
249
|
+
.icon svg {
|
|
250
|
+
width: 40px;
|
|
251
|
+
height: 40px;
|
|
252
|
+
stroke: #fff;
|
|
253
|
+
stroke-width: 3;
|
|
254
|
+
}
|
|
255
|
+
h1 {
|
|
256
|
+
font-size: 1.5rem;
|
|
257
|
+
font-weight: 600;
|
|
258
|
+
margin-bottom: 0.75rem;
|
|
259
|
+
color: #fff;
|
|
260
|
+
}
|
|
261
|
+
p {
|
|
262
|
+
color: #94a3b8;
|
|
263
|
+
font-size: 0.95rem;
|
|
264
|
+
line-height: 1.5;
|
|
265
|
+
}
|
|
266
|
+
.brand {
|
|
267
|
+
margin-top: 2rem;
|
|
268
|
+
opacity: 0.5;
|
|
269
|
+
font-size: 0.8rem;
|
|
270
|
+
letter-spacing: 0.05em;
|
|
271
|
+
}
|
|
272
|
+
</style>
|
|
273
|
+
</head>
|
|
274
|
+
<body>
|
|
275
|
+
<div class="container">
|
|
276
|
+
<div class="icon">
|
|
277
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
|
278
|
+
<polyline points="20 6 9 17 4 12"></polyline>
|
|
279
|
+
</svg>
|
|
280
|
+
</div>
|
|
281
|
+
<h1>Authentication Successful</h1>
|
|
282
|
+
<p>You can close this window and return to your terminal.</p>
|
|
283
|
+
<div class="brand">BERGET</div>
|
|
284
|
+
</div>
|
|
285
|
+
</body>
|
|
286
|
+
</html>
|
|
287
|
+
`);
|
|
288
|
+
server.close();
|
|
289
|
+
resolve({ success: true, code });
|
|
158
290
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
console.log(chalk_1.default.yellow(`\n\nReceived error: ${JSON.stringify(errorObj)}`));
|
|
164
|
-
console.log(chalk_1.default.yellow('Continuing to wait for authentication...'));
|
|
165
|
-
process.stdout.write(`\r${chalk_1.default.blue(spinner[spinnerIdx])} Waiting for authentication...`);
|
|
166
|
-
}
|
|
167
|
-
continue;
|
|
291
|
+
}));
|
|
292
|
+
server.listen(CALLBACK_PORT, () => {
|
|
293
|
+
if (process.argv.includes('--debug')) {
|
|
294
|
+
console.log(chalk_1.default.dim(`Callback server listening on port ${CALLBACK_PORT}`));
|
|
168
295
|
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
console.log(chalk_1.default.
|
|
185
|
-
if (typedTokenData.user) {
|
|
186
|
-
const user = typedTokenData.user;
|
|
187
|
-
console.log(chalk_1.default.green(`Logged in as ${user.name || user.email || 'User'}`));
|
|
188
|
-
}
|
|
189
|
-
return true;
|
|
296
|
+
});
|
|
297
|
+
// Set timeout for the server
|
|
298
|
+
setTimeout(() => {
|
|
299
|
+
server.close();
|
|
300
|
+
resolve({ success: false, error: 'Authentication timed out' });
|
|
301
|
+
}, 5 * 60 * 1000) // 5 minute timeout
|
|
302
|
+
;
|
|
303
|
+
(() => __awaiter(this, void 0, void 0, function* () {
|
|
304
|
+
try {
|
|
305
|
+
const open = yield Promise.resolve().then(() => __importStar(require('open'))).then((m) => m.default);
|
|
306
|
+
yield open(authUrl.toString());
|
|
307
|
+
console.log(chalk_1.default.dim('Browser opened for authentication...'));
|
|
308
|
+
}
|
|
309
|
+
catch (_a) {
|
|
310
|
+
console.log(chalk_1.default.cyan('\nPlease open this URL in your browser:'));
|
|
311
|
+
console.log(chalk_1.default.bold(authUrl.toString()));
|
|
190
312
|
}
|
|
313
|
+
}))();
|
|
314
|
+
});
|
|
315
|
+
if (!authResult.success || !authResult.code) {
|
|
316
|
+
console.log(chalk_1.default.red(`\nAuthentication failed: ${authResult.error || 'Unknown error'}`));
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
// Exchange authorization code for tokens
|
|
320
|
+
console.log(chalk_1.default.dim('Exchanging authorization code for tokens...'));
|
|
321
|
+
const tokenUrl = `${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token`;
|
|
322
|
+
const tokenResponse = yield fetch(tokenUrl, {
|
|
323
|
+
method: 'POST',
|
|
324
|
+
headers: {
|
|
325
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
326
|
+
},
|
|
327
|
+
body: new URLSearchParams({
|
|
328
|
+
grant_type: 'authorization_code',
|
|
329
|
+
client_id: KEYCLOAK_CLIENT_ID,
|
|
330
|
+
code: authResult.code,
|
|
331
|
+
redirect_uri: redirectUri,
|
|
332
|
+
code_verifier: codeVerifier,
|
|
333
|
+
}).toString(),
|
|
334
|
+
});
|
|
335
|
+
if (!tokenResponse.ok) {
|
|
336
|
+
const errorText = yield tokenResponse.text();
|
|
337
|
+
console.log(chalk_1.default.red(`\nFailed to exchange code for tokens: ${errorText}`));
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
const tokenData = (yield tokenResponse.json());
|
|
341
|
+
// Save tokens
|
|
342
|
+
(0, client_1.saveAuthToken)(tokenData.access_token, tokenData.refresh_token, tokenData.expires_in);
|
|
343
|
+
if (process.argv.includes('--debug')) {
|
|
344
|
+
console.log(chalk_1.default.yellow('DEBUG: Token data received:'));
|
|
345
|
+
console.log(chalk_1.default.yellow(JSON.stringify({
|
|
346
|
+
expires_in: tokenData.expires_in,
|
|
347
|
+
refresh_expires_in: tokenData.refresh_expires_in,
|
|
348
|
+
}, null, 2)));
|
|
349
|
+
}
|
|
350
|
+
console.log(chalk_1.default.green('\n✓ Successfully logged in to Berget'));
|
|
351
|
+
// Try to get user info
|
|
352
|
+
try {
|
|
353
|
+
const profile = yield this.whoami();
|
|
354
|
+
if (profile === null || profile === void 0 ? void 0 : profile.email) {
|
|
355
|
+
console.log(chalk_1.default.green(`Logged in as ${profile.name || profile.email}`));
|
|
191
356
|
}
|
|
192
357
|
}
|
|
193
|
-
|
|
194
|
-
|
|
358
|
+
catch (_a) {
|
|
359
|
+
// Ignore errors fetching profile
|
|
360
|
+
}
|
|
361
|
+
console.log(chalk_1.default.cyan('\nNext steps:'));
|
|
362
|
+
console.log(chalk_1.default.cyan(' • Create an API key: berget api-keys create'));
|
|
363
|
+
console.log(chalk_1.default.cyan(' • Setup OpenCode: berget code init'));
|
|
364
|
+
return true;
|
|
195
365
|
}
|
|
196
366
|
catch (error) {
|
|
197
367
|
(0, error_handler_1.handleError)('Login failed', error);
|
|
@@ -104,18 +104,23 @@ class TokenManager {
|
|
|
104
104
|
}
|
|
105
105
|
/**
|
|
106
106
|
* Check if the access token is expired
|
|
107
|
-
* @returns true if expired or about to expire (within
|
|
107
|
+
* @returns true if expired or about to expire (within 10% of lifetime or 30 seconds), false otherwise
|
|
108
108
|
*/
|
|
109
109
|
isTokenExpired() {
|
|
110
110
|
if (!this.tokenData || !this.tokenData.expires_at)
|
|
111
111
|
return true;
|
|
112
112
|
try {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
113
|
+
const now = Date.now();
|
|
114
|
+
const expiresAt = this.tokenData.expires_at;
|
|
115
|
+
const timeUntilExpiry = expiresAt - now;
|
|
116
|
+
// Use 10% of remaining lifetime or 30 seconds, whichever is smaller
|
|
117
|
+
// This ensures we don't refresh tokens that were just issued
|
|
118
|
+
const minBuffer = 30 * 1000; // 30 seconds minimum
|
|
119
|
+
const percentBuffer = timeUntilExpiry * 0.1; // 10% of lifetime
|
|
120
|
+
const expirationBuffer = Math.min(minBuffer, percentBuffer);
|
|
121
|
+
const isExpired = now + expirationBuffer >= expiresAt;
|
|
117
122
|
if (isExpired) {
|
|
118
|
-
logger_1.logger.debug(`Token expired or expiring soon. Current time: ${new Date().toISOString()}, Expiry: ${new Date(
|
|
123
|
+
logger_1.logger.debug(`Token expired or expiring soon. Current time: ${new Date().toISOString()}, Expiry: ${new Date(expiresAt).toISOString()}`);
|
|
119
124
|
}
|
|
120
125
|
return isExpired;
|
|
121
126
|
}
|
package/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ Version: ${version}`
|
|
|
24
24
|
)
|
|
25
25
|
.version(version, '-v, --version')
|
|
26
26
|
.option('--local', 'Use local API endpoint (hidden)', false)
|
|
27
|
+
.option('--stage', 'Use stage API endpoint', false)
|
|
27
28
|
.option('--debug', 'Enable debug output', false)
|
|
28
29
|
|
|
29
30
|
// Register all commands
|