berget 2.1.2 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +20 -6
- package/dist/index.js +2 -1
- package/dist/package.json +1 -1
- package/dist/src/client.js +22 -6
- package/dist/src/commands/code.js +33 -427
- package/dist/src/services/auth-service.js +280 -113
- package/dist/src/utils/token-manager.js +11 -6
- package/index.ts +3 -2
- package/package.json +1 -1
- package/src/client.ts +29 -9
- package/src/commands/code.ts +33 -519
- package/src/services/auth-service.ts +318 -187
- package/src/utils/token-manager.ts +13 -7
- package/dist/src/schemas/opencode-schema.json +0 -1121
- package/dist/src/services/registration-service.js +0 -163
|
@@ -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,126 +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
|
-
console.log(chalk_1.default.cyan('\nNext steps:'));
|
|
190
|
-
console.log(chalk_1.default.cyan(' • Create an API key: berget api-keys create'));
|
|
191
|
-
console.log(chalk_1.default.cyan(' • Setup OpenCode: berget code init'));
|
|
192
|
-
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()));
|
|
193
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}`));
|
|
194
356
|
}
|
|
195
357
|
}
|
|
196
|
-
|
|
197
|
-
|
|
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;
|
|
198
365
|
}
|
|
199
366
|
catch (error) {
|
|
200
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { program } from 'commander'
|
|
3
|
+
import { program, Option } from 'commander'
|
|
4
4
|
import { registerCommands } from './src/commands'
|
|
5
5
|
import { checkBergetConfig } from './src/utils/config-checker'
|
|
6
6
|
import chalk from 'chalk'
|
|
@@ -23,7 +23,8 @@ program
|
|
|
23
23
|
Version: ${version}`
|
|
24
24
|
)
|
|
25
25
|
.version(version, '-v, --version')
|
|
26
|
-
.
|
|
26
|
+
.addOption(new Option('--local').default(false).hideHelp())
|
|
27
|
+
.addOption(new Option('--stage').default(false).hideHelp())
|
|
27
28
|
.option('--debug', 'Enable debug output', false)
|
|
28
29
|
|
|
29
30
|
// Register all commands
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -9,13 +9,20 @@ import { logger } from './utils/logger'
|
|
|
9
9
|
|
|
10
10
|
// API Base URL
|
|
11
11
|
// Use --local flag to test against local API
|
|
12
|
+
// Use --stage flag to test against stage API
|
|
12
13
|
const isLocalMode = process.argv.includes('--local')
|
|
14
|
+
const isStageMode = process.argv.includes('--stage')
|
|
15
|
+
|
|
13
16
|
export const API_BASE_URL =
|
|
14
17
|
process.env.BERGET_API_URL ||
|
|
15
|
-
(isLocalMode ? 'http://localhost:3000' :
|
|
18
|
+
(isLocalMode ? 'http://localhost:3000' :
|
|
19
|
+
isStageMode ? 'https://api.stage.berget.ai' :
|
|
20
|
+
'https://api.berget.ai') // production default
|
|
16
21
|
|
|
17
22
|
if (isLocalMode && !process.env.BERGET_API_URL) {
|
|
18
23
|
logger.debug('Using local API endpoint: http://localhost:3000')
|
|
24
|
+
} else if (isStageMode && !process.env.BERGET_API_URL) {
|
|
25
|
+
logger.debug('Using stage API endpoint: https://api.stage.berget.ai')
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
// Create a typed client for the Berget API
|
|
@@ -209,6 +216,13 @@ export const createAuthenticatedClient = () => {
|
|
|
209
216
|
})
|
|
210
217
|
}
|
|
211
218
|
|
|
219
|
+
// Keycloak configuration for token refresh (must match auth-service.ts)
|
|
220
|
+
const KEYCLOAK_URL = (isStageMode || isLocalMode)
|
|
221
|
+
? 'https://keycloak.stage.berget.ai'
|
|
222
|
+
: 'https://keycloak.berget.ai'
|
|
223
|
+
const KEYCLOAK_REALM = 'berget'
|
|
224
|
+
const KEYCLOAK_CLIENT_ID = 'berget-code'
|
|
225
|
+
|
|
212
226
|
// Helper function to refresh the access token
|
|
213
227
|
async function refreshAccessToken(
|
|
214
228
|
tokenManager: TokenManager,
|
|
@@ -219,16 +233,22 @@ async function refreshAccessToken(
|
|
|
219
233
|
|
|
220
234
|
logger.debug('Attempting to refresh access token')
|
|
221
235
|
|
|
222
|
-
//
|
|
236
|
+
// Refresh directly against Keycloak (berget-code is a public PKCE client)
|
|
223
237
|
try {
|
|
224
|
-
const response = await fetch(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
238
|
+
const response = await fetch(
|
|
239
|
+
`${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token`,
|
|
240
|
+
{
|
|
241
|
+
method: 'POST',
|
|
242
|
+
headers: {
|
|
243
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
244
|
+
},
|
|
245
|
+
body: new URLSearchParams({
|
|
246
|
+
grant_type: 'refresh_token',
|
|
247
|
+
client_id: KEYCLOAK_CLIENT_ID,
|
|
248
|
+
refresh_token: refreshToken,
|
|
249
|
+
}),
|
|
229
250
|
},
|
|
230
|
-
|
|
231
|
-
})
|
|
251
|
+
)
|
|
232
252
|
|
|
233
253
|
// Handle HTTP errors
|
|
234
254
|
if (!response.ok) {
|