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
|
@@ -3,11 +3,39 @@ import {
|
|
|
3
3
|
saveAuthToken,
|
|
4
4
|
clearAuthToken,
|
|
5
5
|
apiClient,
|
|
6
|
+
API_BASE_URL,
|
|
6
7
|
} from '../client'
|
|
7
8
|
// We'll use dynamic import for 'open' to support ESM modules in CommonJS
|
|
8
9
|
import chalk from 'chalk'
|
|
9
10
|
import { handleError } from '../utils/error-handler'
|
|
10
11
|
import { COMMAND_GROUPS, SUBCOMMANDS } from '../constants/command-structure'
|
|
12
|
+
import * as http from 'http'
|
|
13
|
+
import * as crypto from 'crypto'
|
|
14
|
+
import * as url from 'url'
|
|
15
|
+
|
|
16
|
+
// Keycloak configuration based on environment
|
|
17
|
+
const isStageMode = process.argv.includes('--stage')
|
|
18
|
+
const isLocalMode = process.argv.includes('--local')
|
|
19
|
+
const KEYCLOAK_URL = (isStageMode || isLocalMode)
|
|
20
|
+
? 'https://keycloak.stage.berget.ai'
|
|
21
|
+
: 'https://keycloak.berget.ai'
|
|
22
|
+
const KEYCLOAK_REALM = 'berget'
|
|
23
|
+
const KEYCLOAK_CLIENT_ID = 'berget-code'
|
|
24
|
+
const CALLBACK_PORT = 8787
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generate a random string for PKCE code_verifier
|
|
28
|
+
*/
|
|
29
|
+
function generateCodeVerifier(): string {
|
|
30
|
+
return crypto.randomBytes(32).toString('base64url')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generate code_challenge from code_verifier using S256 method
|
|
35
|
+
*/
|
|
36
|
+
function generateCodeChallenge(verifier: string): string {
|
|
37
|
+
return crypto.createHash('sha256').update(verifier).digest('base64url')
|
|
38
|
+
}
|
|
11
39
|
|
|
12
40
|
/**
|
|
13
41
|
* Service for authentication operations
|
|
@@ -34,7 +62,9 @@ export class AuthService {
|
|
|
34
62
|
|
|
35
63
|
public async whoami(): Promise<any> {
|
|
36
64
|
try {
|
|
37
|
-
|
|
65
|
+
// Create fresh client to ensure we have the latest token
|
|
66
|
+
const client = createAuthenticatedClient()
|
|
67
|
+
const { data: profile, error } = await client.GET('/v1/users/me')
|
|
38
68
|
if (error) {
|
|
39
69
|
return null
|
|
40
70
|
}
|
|
@@ -51,209 +81,314 @@ export class AuthService {
|
|
|
51
81
|
|
|
52
82
|
console.log(chalk.blue('Initiating login process...'))
|
|
53
83
|
|
|
54
|
-
//
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
)
|
|
84
|
+
// Generate PKCE code verifier and challenge
|
|
85
|
+
const codeVerifier = generateCodeVerifier()
|
|
86
|
+
const codeChallenge = generateCodeChallenge(codeVerifier)
|
|
87
|
+
const state = crypto.randomBytes(16).toString('hex')
|
|
59
88
|
|
|
60
|
-
|
|
61
|
-
throw new Error(
|
|
62
|
-
deviceError
|
|
63
|
-
? JSON.stringify(deviceError)
|
|
64
|
-
: 'Failed to get device authorization data',
|
|
65
|
-
)
|
|
66
|
-
}
|
|
89
|
+
const redirectUri = `http://localhost:${CALLBACK_PORT}/callback`
|
|
67
90
|
|
|
68
|
-
//
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
user_code?: string
|
|
72
|
-
device_code?: string
|
|
73
|
-
expires_in?: number
|
|
74
|
-
interval?: number
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Display information to user
|
|
78
|
-
console.log(chalk.cyan('\nTo complete login:'))
|
|
79
|
-
console.log(
|
|
80
|
-
chalk.cyan(
|
|
81
|
-
`1. Open this URL: ${chalk.bold(
|
|
82
|
-
typedDeviceData.verification_url ||
|
|
83
|
-
'https://keycloak.berget.ai/device',
|
|
84
|
-
)}`,
|
|
85
|
-
),
|
|
91
|
+
// Build authorization URL
|
|
92
|
+
const authUrl = new URL(
|
|
93
|
+
`${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/auth`,
|
|
86
94
|
)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
)
|
|
95
|
+
authUrl.searchParams.set('client_id', KEYCLOAK_CLIENT_ID)
|
|
96
|
+
authUrl.searchParams.set('response_type', 'code')
|
|
97
|
+
authUrl.searchParams.set('redirect_uri', redirectUri)
|
|
98
|
+
authUrl.searchParams.set('scope', 'openid email profile')
|
|
99
|
+
authUrl.searchParams.set('state', state)
|
|
100
|
+
authUrl.searchParams.set('code_challenge', codeChallenge)
|
|
101
|
+
authUrl.searchParams.set('code_challenge_method', 'S256')
|
|
95
102
|
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
"Browser opened automatically. If it didn't open, please use the URL above.",
|
|
105
|
-
),
|
|
106
|
-
)
|
|
107
|
-
}
|
|
108
|
-
} catch (error) {
|
|
109
|
-
console.log(
|
|
110
|
-
chalk.yellow(
|
|
111
|
-
'Could not open browser automatically. Please open the URL manually.',
|
|
112
|
-
),
|
|
113
|
-
)
|
|
114
|
-
}
|
|
103
|
+
// Create a promise that resolves when we receive the callback
|
|
104
|
+
const authResult = await new Promise<{
|
|
105
|
+
success: boolean
|
|
106
|
+
code?: string
|
|
107
|
+
error?: string
|
|
108
|
+
}>((resolve) => {
|
|
109
|
+
const server = http.createServer(async (req, res) => {
|
|
110
|
+
const parsedUrl = url.parse(req.url || '', true)
|
|
115
111
|
|
|
116
|
-
|
|
112
|
+
if (parsedUrl.pathname === '/callback') {
|
|
113
|
+
const receivedState = parsedUrl.query.state as string
|
|
114
|
+
const code = parsedUrl.query.code as string
|
|
115
|
+
const error = parsedUrl.query.error as string
|
|
117
116
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
117
|
+
const errorPage = (title: string, message: string) => `
|
|
118
|
+
<!DOCTYPE html>
|
|
119
|
+
<html lang="en">
|
|
120
|
+
<head>
|
|
121
|
+
<meta charset="UTF-8">
|
|
122
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
123
|
+
<title>Berget - Authentication Failed</title>
|
|
124
|
+
<style>
|
|
125
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
126
|
+
body {
|
|
127
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
128
|
+
display: flex;
|
|
129
|
+
justify-content: center;
|
|
130
|
+
align-items: center;
|
|
131
|
+
min-height: 100vh;
|
|
132
|
+
background: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 50%, #16213e 100%);
|
|
133
|
+
color: #fff;
|
|
134
|
+
}
|
|
135
|
+
.container {
|
|
136
|
+
text-align: center;
|
|
137
|
+
padding: 3rem;
|
|
138
|
+
max-width: 400px;
|
|
139
|
+
}
|
|
140
|
+
.icon {
|
|
141
|
+
width: 80px;
|
|
142
|
+
height: 80px;
|
|
143
|
+
background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
|
|
144
|
+
border-radius: 50%;
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
justify-content: center;
|
|
148
|
+
margin: 0 auto 1.5rem;
|
|
149
|
+
box-shadow: 0 4px 20px rgba(248, 113, 113, 0.3);
|
|
150
|
+
}
|
|
151
|
+
.icon svg {
|
|
152
|
+
width: 40px;
|
|
153
|
+
height: 40px;
|
|
154
|
+
stroke: #fff;
|
|
155
|
+
stroke-width: 3;
|
|
156
|
+
}
|
|
157
|
+
h1 {
|
|
158
|
+
font-size: 1.5rem;
|
|
159
|
+
font-weight: 600;
|
|
160
|
+
margin-bottom: 0.75rem;
|
|
161
|
+
color: #fff;
|
|
162
|
+
}
|
|
163
|
+
p {
|
|
164
|
+
color: #94a3b8;
|
|
165
|
+
font-size: 0.95rem;
|
|
166
|
+
line-height: 1.5;
|
|
167
|
+
}
|
|
168
|
+
.brand {
|
|
169
|
+
margin-top: 2rem;
|
|
170
|
+
opacity: 0.5;
|
|
171
|
+
font-size: 0.8rem;
|
|
172
|
+
letter-spacing: 0.05em;
|
|
173
|
+
}
|
|
174
|
+
</style>
|
|
175
|
+
</head>
|
|
176
|
+
<body>
|
|
177
|
+
<div class="container">
|
|
178
|
+
<div class="icon">
|
|
179
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
|
180
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
181
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
182
|
+
</svg>
|
|
183
|
+
</div>
|
|
184
|
+
<h1>${title}</h1>
|
|
185
|
+
<p>${message}</p>
|
|
186
|
+
<div class="brand">BERGET</div>
|
|
187
|
+
</div>
|
|
188
|
+
</body>
|
|
189
|
+
</html>
|
|
190
|
+
`
|
|
126
191
|
|
|
127
|
-
|
|
128
|
-
|
|
192
|
+
if (error) {
|
|
193
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
|
|
194
|
+
res.end(errorPage('Authentication Failed', String(parsedUrl.query.error_description || error)))
|
|
195
|
+
server.close()
|
|
196
|
+
resolve({ success: false, error })
|
|
197
|
+
return
|
|
198
|
+
}
|
|
129
199
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
200
|
+
if (receivedState !== state) {
|
|
201
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
|
|
202
|
+
res.end(errorPage('Authentication Failed', 'Invalid state parameter. Please try again.'))
|
|
203
|
+
server.close()
|
|
204
|
+
resolve({ success: false, error: 'Invalid state parameter' })
|
|
205
|
+
return
|
|
206
|
+
}
|
|
133
207
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
208
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
|
|
209
|
+
res.end(`
|
|
210
|
+
<!DOCTYPE html>
|
|
211
|
+
<html lang="en">
|
|
212
|
+
<head>
|
|
213
|
+
<meta charset="UTF-8">
|
|
214
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
215
|
+
<title>Berget - Authentication Successful</title>
|
|
216
|
+
<style>
|
|
217
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
218
|
+
body {
|
|
219
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
220
|
+
display: flex;
|
|
221
|
+
justify-content: center;
|
|
222
|
+
align-items: center;
|
|
223
|
+
min-height: 100vh;
|
|
224
|
+
background: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 50%, #16213e 100%);
|
|
225
|
+
color: #fff;
|
|
226
|
+
}
|
|
227
|
+
.container {
|
|
228
|
+
text-align: center;
|
|
229
|
+
padding: 3rem;
|
|
230
|
+
max-width: 400px;
|
|
231
|
+
}
|
|
232
|
+
.icon {
|
|
233
|
+
width: 80px;
|
|
234
|
+
height: 80px;
|
|
235
|
+
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
|
|
236
|
+
border-radius: 50%;
|
|
237
|
+
display: flex;
|
|
238
|
+
align-items: center;
|
|
239
|
+
justify-content: center;
|
|
240
|
+
margin: 0 auto 1.5rem;
|
|
241
|
+
box-shadow: 0 4px 20px rgba(74, 222, 128, 0.3);
|
|
242
|
+
}
|
|
243
|
+
.icon svg {
|
|
244
|
+
width: 40px;
|
|
245
|
+
height: 40px;
|
|
246
|
+
stroke: #fff;
|
|
247
|
+
stroke-width: 3;
|
|
248
|
+
}
|
|
249
|
+
h1 {
|
|
250
|
+
font-size: 1.5rem;
|
|
251
|
+
font-weight: 600;
|
|
252
|
+
margin-bottom: 0.75rem;
|
|
253
|
+
color: #fff;
|
|
254
|
+
}
|
|
255
|
+
p {
|
|
256
|
+
color: #94a3b8;
|
|
257
|
+
font-size: 0.95rem;
|
|
258
|
+
line-height: 1.5;
|
|
259
|
+
}
|
|
260
|
+
.brand {
|
|
261
|
+
margin-top: 2rem;
|
|
262
|
+
opacity: 0.5;
|
|
263
|
+
font-size: 0.8rem;
|
|
264
|
+
letter-spacing: 0.05em;
|
|
265
|
+
}
|
|
266
|
+
</style>
|
|
267
|
+
</head>
|
|
268
|
+
<body>
|
|
269
|
+
<div class="container">
|
|
270
|
+
<div class="icon">
|
|
271
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
|
272
|
+
<polyline points="20 6 9 17 4 12"></polyline>
|
|
273
|
+
</svg>
|
|
274
|
+
</div>
|
|
275
|
+
<h1>Authentication Successful</h1>
|
|
276
|
+
<p>You can close this window and return to your terminal.</p>
|
|
277
|
+
<div class="brand">BERGET</div>
|
|
278
|
+
</div>
|
|
279
|
+
</body>
|
|
280
|
+
</html>
|
|
281
|
+
`)
|
|
282
|
+
server.close()
|
|
283
|
+
resolve({ success: true, code })
|
|
284
|
+
}
|
|
285
|
+
})
|
|
150
286
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const status = errorObj.status || 0
|
|
157
|
-
const errorCode = errorObj.code || ''
|
|
158
|
-
|
|
159
|
-
if (status === 401 || errorCode === 'AUTHORIZATION_PENDING') {
|
|
160
|
-
// Still waiting for user to complete authorization
|
|
161
|
-
continue
|
|
162
|
-
} else if (status === 429) {
|
|
163
|
-
// Slow down
|
|
164
|
-
pollInterval *= 2
|
|
165
|
-
continue
|
|
166
|
-
} else if (status === 400) {
|
|
167
|
-
// Error or expired
|
|
168
|
-
if (errorCode === 'EXPIRED_TOKEN') {
|
|
169
|
-
console.log(
|
|
170
|
-
chalk.red('\n\nAuthentication timed out. Please try again.'),
|
|
171
|
-
)
|
|
172
|
-
} else if (errorCode !== 'AUTHORIZATION_PENDING') {
|
|
173
|
-
// Only show error if it's not the expected "still waiting" error
|
|
174
|
-
const errorMessage = errorObj.message || JSON.stringify(errorObj)
|
|
175
|
-
console.log(chalk.red(`\n\nError: ${errorMessage}`))
|
|
176
|
-
return false
|
|
177
|
-
} else {
|
|
178
|
-
// If it's AUTHORIZATION_PENDING, continue polling
|
|
179
|
-
continue
|
|
180
|
-
}
|
|
181
|
-
return false
|
|
182
|
-
} else {
|
|
183
|
-
// For any other error, log it but continue polling
|
|
184
|
-
// This makes the flow more resilient to temporary issues
|
|
185
|
-
if (process.env.DEBUG) {
|
|
186
|
-
console.log(
|
|
187
|
-
chalk.yellow(`\n\nReceived error: ${JSON.stringify(errorObj)}`),
|
|
188
|
-
)
|
|
189
|
-
console.log(
|
|
190
|
-
chalk.yellow('Continuing to wait for authentication...'),
|
|
191
|
-
)
|
|
192
|
-
process.stdout.write(
|
|
193
|
-
`\r${chalk.blue(
|
|
194
|
-
spinner[spinnerIdx],
|
|
195
|
-
)} Waiting for authentication...`,
|
|
196
|
-
)
|
|
197
|
-
}
|
|
198
|
-
continue
|
|
287
|
+
server.listen(CALLBACK_PORT, () => {
|
|
288
|
+
if (process.argv.includes('--debug')) {
|
|
289
|
+
console.log(
|
|
290
|
+
chalk.dim(`Callback server listening on port ${CALLBACK_PORT}`),
|
|
291
|
+
)
|
|
199
292
|
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
// Set timeout for the server
|
|
296
|
+
setTimeout(() => {
|
|
297
|
+
server.close()
|
|
298
|
+
resolve({ success: false, error: 'Authentication timed out' })
|
|
299
|
+
}, 5 * 60 * 1000) // 5 minute timeout
|
|
300
|
+
|
|
301
|
+
// Open browser
|
|
302
|
+
;(async () => {
|
|
303
|
+
try {
|
|
304
|
+
const open = await import('open').then((m) => m.default)
|
|
305
|
+
await open(authUrl.toString())
|
|
306
|
+
console.log(chalk.dim('Browser opened for authentication...'))
|
|
307
|
+
} catch {
|
|
308
|
+
console.log(chalk.cyan('\nPlease open this URL in your browser:'))
|
|
309
|
+
console.log(chalk.bold(authUrl.toString()))
|
|
212
310
|
}
|
|
311
|
+
})()
|
|
312
|
+
})
|
|
213
313
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
)
|
|
314
|
+
if (!authResult.success || !authResult.code) {
|
|
315
|
+
console.log(
|
|
316
|
+
chalk.red(`\nAuthentication failed: ${authResult.error || 'Unknown error'}`),
|
|
317
|
+
)
|
|
318
|
+
return false
|
|
319
|
+
}
|
|
221
320
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
console.log(
|
|
225
|
-
chalk.yellow(
|
|
226
|
-
JSON.stringify(
|
|
227
|
-
{
|
|
228
|
-
expires_in: typedTokenData.expires_in,
|
|
229
|
-
refresh_expires_in: typedTokenData.refresh_expires_in,
|
|
230
|
-
},
|
|
231
|
-
null,
|
|
232
|
-
2,
|
|
233
|
-
),
|
|
234
|
-
),
|
|
235
|
-
)
|
|
236
|
-
}
|
|
321
|
+
// Exchange authorization code for tokens
|
|
322
|
+
console.log(chalk.dim('Exchanging authorization code for tokens...'))
|
|
237
323
|
|
|
238
|
-
|
|
239
|
-
|
|
324
|
+
const tokenUrl = `${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token`
|
|
325
|
+
const tokenResponse = await fetch(tokenUrl, {
|
|
326
|
+
method: 'POST',
|
|
327
|
+
headers: {
|
|
328
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
329
|
+
},
|
|
330
|
+
body: new URLSearchParams({
|
|
331
|
+
grant_type: 'authorization_code',
|
|
332
|
+
client_id: KEYCLOAK_CLIENT_ID,
|
|
333
|
+
code: authResult.code,
|
|
334
|
+
redirect_uri: redirectUri,
|
|
335
|
+
code_verifier: codeVerifier,
|
|
336
|
+
}).toString(),
|
|
337
|
+
})
|
|
240
338
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
),
|
|
247
|
-
)
|
|
248
|
-
}
|
|
339
|
+
if (!tokenResponse.ok) {
|
|
340
|
+
const errorText = await tokenResponse.text()
|
|
341
|
+
console.log(chalk.red(`\nFailed to exchange code for tokens: ${errorText}`))
|
|
342
|
+
return false
|
|
343
|
+
}
|
|
249
344
|
|
|
250
|
-
|
|
251
|
-
|
|
345
|
+
const tokenData = (await tokenResponse.json()) as {
|
|
346
|
+
access_token: string
|
|
347
|
+
refresh_token: string
|
|
348
|
+
expires_in: number
|
|
349
|
+
refresh_expires_in?: number
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Save tokens
|
|
353
|
+
saveAuthToken(
|
|
354
|
+
tokenData.access_token,
|
|
355
|
+
tokenData.refresh_token,
|
|
356
|
+
tokenData.expires_in,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
if (process.argv.includes('--debug')) {
|
|
360
|
+
console.log(chalk.yellow('DEBUG: Token data received:'))
|
|
361
|
+
console.log(
|
|
362
|
+
chalk.yellow(
|
|
363
|
+
JSON.stringify(
|
|
364
|
+
{
|
|
365
|
+
expires_in: tokenData.expires_in,
|
|
366
|
+
refresh_expires_in: tokenData.refresh_expires_in,
|
|
367
|
+
},
|
|
368
|
+
null,
|
|
369
|
+
2,
|
|
370
|
+
),
|
|
371
|
+
),
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
console.log(chalk.green('\n✓ Successfully logged in to Berget'))
|
|
376
|
+
|
|
377
|
+
// Try to get user info
|
|
378
|
+
try {
|
|
379
|
+
const profile = await this.whoami()
|
|
380
|
+
if (profile?.email) {
|
|
381
|
+
console.log(chalk.green(`Logged in as ${profile.name || profile.email}`))
|
|
252
382
|
}
|
|
383
|
+
} catch {
|
|
384
|
+
// Ignore errors fetching profile
|
|
253
385
|
}
|
|
254
386
|
|
|
255
|
-
console.log(chalk.
|
|
256
|
-
|
|
387
|
+
console.log(chalk.cyan('\nNext steps:'))
|
|
388
|
+
console.log(chalk.cyan(' • Create an API key: berget api-keys create'))
|
|
389
|
+
console.log(chalk.cyan(' • Setup OpenCode: berget code init'))
|
|
390
|
+
|
|
391
|
+
return true
|
|
257
392
|
} catch (error) {
|
|
258
393
|
handleError('Login failed', error)
|
|
259
394
|
return false
|
|
@@ -93,21 +93,27 @@ export class TokenManager {
|
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
95
|
* Check if the access token is expired
|
|
96
|
-
* @returns true if expired or about to expire (within
|
|
96
|
+
* @returns true if expired or about to expire (within 10% of lifetime or 30 seconds), false otherwise
|
|
97
97
|
*/
|
|
98
98
|
public isTokenExpired(): boolean {
|
|
99
99
|
if (!this.tokenData || !this.tokenData.expires_at) return true
|
|
100
100
|
|
|
101
101
|
try {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
const now = Date.now()
|
|
103
|
+
const expiresAt = this.tokenData.expires_at
|
|
104
|
+
const timeUntilExpiry = expiresAt - now
|
|
105
|
+
|
|
106
|
+
// Use 10% of remaining lifetime or 30 seconds, whichever is smaller
|
|
107
|
+
// This ensures we don't refresh tokens that were just issued
|
|
108
|
+
const minBuffer = 30 * 1000 // 30 seconds minimum
|
|
109
|
+
const percentBuffer = timeUntilExpiry * 0.1 // 10% of lifetime
|
|
110
|
+
const expirationBuffer = Math.min(minBuffer, percentBuffer)
|
|
111
|
+
|
|
112
|
+
const isExpired = now + expirationBuffer >= expiresAt
|
|
107
113
|
|
|
108
114
|
if (isExpired) {
|
|
109
115
|
logger.debug(
|
|
110
|
-
`Token expired or expiring soon. Current time: ${new Date().toISOString()}, Expiry: ${new Date(
|
|
116
|
+
`Token expired or expiring soon. Current time: ${new Date().toISOString()}, Expiry: ${new Date(expiresAt).toISOString()}`,
|
|
111
117
|
)
|
|
112
118
|
}
|
|
113
119
|
|