@adversity/coding-tool-x 3.0.6 → 3.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/CHANGELOG.md +23 -0
- package/dist/web/assets/{icons-BxudHPiX.js → icons-CO_2OFES.js} +1 -1
- package/dist/web/assets/{index-D2VfwJBa.js → index-DI8QOi-E.js} +2 -2
- package/dist/web/assets/{index-oXBzu0bd.css → index-uLHGdeZh.css} +1 -1
- package/dist/web/assets/{naive-ui-DT-Uur8K.js → naive-ui-B1re3c-e.js} +1 -1
- package/dist/web/index.html +4 -4
- package/package.json +1 -1
- package/src/commands/daemon.js +11 -1
- package/src/commands/ui.js +8 -1
- package/src/index.js +3 -1
- package/src/server/api/oauth.js +294 -0
- package/src/server/codex-proxy-server.js +3 -2
- package/src/server/config/oauth-providers.js +68 -0
- package/src/server/gemini-proxy-server.js +3 -1
- package/src/server/index.js +12 -3
- package/src/server/proxy-server.js +4 -2
- package/src/server/services/channels.js +33 -2
- package/src/server/services/codex-channels.js +27 -2
- package/src/server/services/gemini-channels.js +34 -1
- package/src/server/services/oauth-callback-server.js +284 -0
- package/src/server/services/oauth-service.js +378 -0
- package/src/server/services/oauth-token-storage.js +135 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const http = require('http');
|
|
6
|
+
const { URL } = require('url');
|
|
7
|
+
const { getProviderConfig } = require('../config/oauth-providers');
|
|
8
|
+
|
|
9
|
+
// Pending OAuth flows storage with state -> flow data mapping
|
|
10
|
+
const pendingFlows = new Map();
|
|
11
|
+
|
|
12
|
+
// Flow expiry time: 10 minutes
|
|
13
|
+
const FLOW_EXPIRY_MS = 10 * 60 * 1000;
|
|
14
|
+
|
|
15
|
+
// ============================================
|
|
16
|
+
// PKCE Generation
|
|
17
|
+
// ============================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate base64url encoded string from buffer
|
|
21
|
+
* @param {Buffer} buffer
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
function base64url(buffer) {
|
|
25
|
+
return buffer
|
|
26
|
+
.toString('base64')
|
|
27
|
+
.replace(/\+/g, '-')
|
|
28
|
+
.replace(/\//g, '_')
|
|
29
|
+
.replace(/=/g, '');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Generate PKCE code verifier and challenge
|
|
34
|
+
* @returns {{ codeVerifier: string, codeChallenge: string }}
|
|
35
|
+
*/
|
|
36
|
+
function generatePKCE() {
|
|
37
|
+
// 32 bytes random -> base64url for code verifier
|
|
38
|
+
const codeVerifier = base64url(crypto.randomBytes(32));
|
|
39
|
+
|
|
40
|
+
// SHA-256 hash of verifier -> base64url for challenge
|
|
41
|
+
const hash = crypto.createHash('sha256').update(codeVerifier).digest();
|
|
42
|
+
const codeChallenge = base64url(hash);
|
|
43
|
+
|
|
44
|
+
return { codeVerifier, codeChallenge };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ============================================
|
|
48
|
+
// State Management
|
|
49
|
+
// ============================================
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generate random state for OAuth flow
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
function generateState() {
|
|
56
|
+
return crypto.randomBytes(16).toString('hex');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Clean up expired flows (older than 10 minutes)
|
|
61
|
+
*/
|
|
62
|
+
function cleanupExpiredFlows() {
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
for (const [state, flow] of pendingFlows.entries()) {
|
|
65
|
+
if (now - flow.createdAt > FLOW_EXPIRY_MS) {
|
|
66
|
+
pendingFlows.delete(state);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================
|
|
72
|
+
// Auth URL Construction
|
|
73
|
+
// ============================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build OAuth authorization URL
|
|
77
|
+
* @param {string} provider - Provider name (claude, codex, gemini)
|
|
78
|
+
* @param {string} state - State parameter
|
|
79
|
+
* @param {string} codeChallenge - PKCE code challenge (for PKCE providers)
|
|
80
|
+
* @returns {string} Full authorization URL
|
|
81
|
+
*/
|
|
82
|
+
function buildAuthUrl(provider, state, codeChallenge) {
|
|
83
|
+
const config = getProviderConfig(provider);
|
|
84
|
+
const url = new URL(config.authUrl);
|
|
85
|
+
|
|
86
|
+
// Common parameters
|
|
87
|
+
url.searchParams.set('client_id', config.clientId);
|
|
88
|
+
url.searchParams.set('redirect_uri', config.redirectUri);
|
|
89
|
+
url.searchParams.set('response_type', 'code');
|
|
90
|
+
url.searchParams.set('state', state);
|
|
91
|
+
url.searchParams.set('scope', config.scopes.join(' '));
|
|
92
|
+
|
|
93
|
+
// PKCE parameters for PKCE-enabled providers
|
|
94
|
+
if (config.authType === 'pkce' && codeChallenge) {
|
|
95
|
+
url.searchParams.set('code_challenge', codeChallenge);
|
|
96
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Extra parameters from provider config
|
|
100
|
+
if (config.extraParams) {
|
|
101
|
+
for (const [key, value] of Object.entries(config.extraParams)) {
|
|
102
|
+
url.searchParams.set(key, value);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return url.toString();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ============================================
|
|
110
|
+
// HTTP Request Helper
|
|
111
|
+
// ============================================
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Make HTTP/HTTPS request
|
|
115
|
+
* @param {string} urlString - Full URL
|
|
116
|
+
* @param {Object} options - Request options
|
|
117
|
+
* @param {string} body - Request body
|
|
118
|
+
* @returns {Promise<{ statusCode: number, data: Object }>}
|
|
119
|
+
*/
|
|
120
|
+
function makeRequest(urlString, options, body) {
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
const url = new URL(urlString);
|
|
123
|
+
const client = url.protocol === 'https:' ? https : http;
|
|
124
|
+
|
|
125
|
+
const reqOptions = {
|
|
126
|
+
hostname: url.hostname,
|
|
127
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
128
|
+
path: url.pathname + url.search,
|
|
129
|
+
method: options.method || 'POST',
|
|
130
|
+
headers: options.headers || {}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const req = client.request(reqOptions, (res) => {
|
|
134
|
+
let data = '';
|
|
135
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
136
|
+
res.on('end', () => {
|
|
137
|
+
try {
|
|
138
|
+
const parsed = JSON.parse(data);
|
|
139
|
+
resolve({ statusCode: res.statusCode, data: parsed });
|
|
140
|
+
} catch (e) {
|
|
141
|
+
reject(new Error(`Failed to parse response: ${data}`));
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
req.on('error', reject);
|
|
147
|
+
req.on('timeout', () => {
|
|
148
|
+
req.destroy();
|
|
149
|
+
reject(new Error('Request timeout'));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (body) {
|
|
153
|
+
req.write(body);
|
|
154
|
+
}
|
|
155
|
+
req.end();
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ============================================
|
|
160
|
+
// Token Exchange
|
|
161
|
+
// ============================================
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Exchange authorization code for tokens
|
|
165
|
+
* @param {string} provider - Provider name
|
|
166
|
+
* @param {string} code - Authorization code
|
|
167
|
+
* @param {string} codeVerifier - PKCE code verifier (for PKCE providers)
|
|
168
|
+
* @returns {Promise<{ accessToken: string, refreshToken?: string, idToken?: string, expiresIn?: number, scope?: string }>}
|
|
169
|
+
*/
|
|
170
|
+
async function exchangeCodeForToken(provider, code, codeVerifier) {
|
|
171
|
+
const config = getProviderConfig(provider);
|
|
172
|
+
|
|
173
|
+
// Build token request body
|
|
174
|
+
const params = new URLSearchParams();
|
|
175
|
+
params.set('grant_type', 'authorization_code');
|
|
176
|
+
params.set('code', code);
|
|
177
|
+
params.set('client_id', config.clientId);
|
|
178
|
+
params.set('redirect_uri', config.redirectUri);
|
|
179
|
+
|
|
180
|
+
// Add client_secret for standard OAuth (gemini)
|
|
181
|
+
if (config.clientSecret) {
|
|
182
|
+
params.set('client_secret', config.clientSecret);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Add code_verifier for PKCE providers
|
|
186
|
+
if (config.authType === 'pkce' && codeVerifier) {
|
|
187
|
+
params.set('code_verifier', codeVerifier);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Determine content type - Codex requires form-urlencoded
|
|
191
|
+
const contentType = config.tokenContentType || 'application/x-www-form-urlencoded';
|
|
192
|
+
|
|
193
|
+
const headers = {
|
|
194
|
+
'Content-Type': contentType,
|
|
195
|
+
'Accept': 'application/json'
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const { statusCode, data } = await makeRequest(config.tokenUrl, { headers }, params.toString());
|
|
199
|
+
|
|
200
|
+
if (statusCode !== 200) {
|
|
201
|
+
const errorMsg = data.error_description || data.error || 'Token exchange failed';
|
|
202
|
+
throw new Error(`Token exchange failed (${statusCode}): ${errorMsg}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
accessToken: data.access_token,
|
|
207
|
+
refreshToken: data.refresh_token,
|
|
208
|
+
idToken: data.id_token,
|
|
209
|
+
expiresIn: data.expires_in,
|
|
210
|
+
scope: data.scope
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ============================================
|
|
215
|
+
// Token Refresh
|
|
216
|
+
// ============================================
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Refresh access token using refresh token
|
|
220
|
+
* @param {string} provider - Provider name
|
|
221
|
+
* @param {string} refreshTokenValue - Refresh token
|
|
222
|
+
* @returns {Promise<{ accessToken: string, refreshToken?: string, expiresIn?: number, scope?: string }>}
|
|
223
|
+
*/
|
|
224
|
+
async function refreshToken(provider, refreshTokenValue) {
|
|
225
|
+
const config = getProviderConfig(provider);
|
|
226
|
+
|
|
227
|
+
// Build refresh request body
|
|
228
|
+
const params = new URLSearchParams();
|
|
229
|
+
params.set('grant_type', 'refresh_token');
|
|
230
|
+
params.set('refresh_token', refreshTokenValue);
|
|
231
|
+
params.set('client_id', config.clientId);
|
|
232
|
+
|
|
233
|
+
// Add client_secret for standard OAuth (gemini)
|
|
234
|
+
if (config.clientSecret) {
|
|
235
|
+
params.set('client_secret', config.clientSecret);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Determine content type
|
|
239
|
+
const contentType = config.tokenContentType || 'application/x-www-form-urlencoded';
|
|
240
|
+
|
|
241
|
+
const headers = {
|
|
242
|
+
'Content-Type': contentType,
|
|
243
|
+
'Accept': 'application/json'
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const { statusCode, data } = await makeRequest(config.tokenUrl, { headers }, params.toString());
|
|
247
|
+
|
|
248
|
+
if (statusCode !== 200) {
|
|
249
|
+
const errorMsg = data.error_description || data.error || 'Token refresh failed';
|
|
250
|
+
throw new Error(`Token refresh failed (${statusCode}): ${errorMsg}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
accessToken: data.access_token,
|
|
255
|
+
refreshToken: data.refresh_token,
|
|
256
|
+
expiresIn: data.expires_in,
|
|
257
|
+
scope: data.scope
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ============================================
|
|
262
|
+
// Flow Management
|
|
263
|
+
// ============================================
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Start a new OAuth flow
|
|
267
|
+
* @param {string} provider - Provider name
|
|
268
|
+
* @param {string} [channelId] - Optional channel ID to associate with flow
|
|
269
|
+
* @returns {{ state: string, authUrl: string }}
|
|
270
|
+
*/
|
|
271
|
+
function startFlow(provider, channelId) {
|
|
272
|
+
// Clean up old flows first
|
|
273
|
+
cleanupExpiredFlows();
|
|
274
|
+
|
|
275
|
+
const state = generateState();
|
|
276
|
+
const config = getProviderConfig(provider);
|
|
277
|
+
|
|
278
|
+
let codeVerifier = null;
|
|
279
|
+
let codeChallenge = null;
|
|
280
|
+
|
|
281
|
+
// Generate PKCE for PKCE-enabled providers
|
|
282
|
+
if (config.authType === 'pkce') {
|
|
283
|
+
const pkce = generatePKCE();
|
|
284
|
+
codeVerifier = pkce.codeVerifier;
|
|
285
|
+
codeChallenge = pkce.codeChallenge;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const authUrl = buildAuthUrl(provider, state, codeChallenge);
|
|
289
|
+
|
|
290
|
+
// Store flow data
|
|
291
|
+
pendingFlows.set(state, {
|
|
292
|
+
provider,
|
|
293
|
+
codeVerifier,
|
|
294
|
+
channelId: channelId || null,
|
|
295
|
+
createdAt: Date.now(),
|
|
296
|
+
status: 'pending',
|
|
297
|
+
tokenId: null,
|
|
298
|
+
error: null
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
return { state, authUrl };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Get pending flow by state
|
|
306
|
+
* @param {string} state
|
|
307
|
+
* @returns {Object|null}
|
|
308
|
+
*/
|
|
309
|
+
function getPendingFlow(state) {
|
|
310
|
+
return pendingFlows.get(state) || null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Update flow with partial data
|
|
315
|
+
* @param {string} state
|
|
316
|
+
* @param {Object} updates
|
|
317
|
+
* @returns {boolean}
|
|
318
|
+
*/
|
|
319
|
+
function updateFlow(state, updates) {
|
|
320
|
+
const flow = pendingFlows.get(state);
|
|
321
|
+
if (!flow) return false;
|
|
322
|
+
|
|
323
|
+
Object.assign(flow, updates);
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Mark flow as completed
|
|
329
|
+
* @param {string} state
|
|
330
|
+
* @param {string} tokenId - Token ID from storage
|
|
331
|
+
* @returns {boolean}
|
|
332
|
+
*/
|
|
333
|
+
function completeFlow(state, tokenId) {
|
|
334
|
+
return updateFlow(state, { status: 'completed', tokenId });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Mark flow as failed
|
|
339
|
+
* @param {string} state
|
|
340
|
+
* @param {string} error - Error message
|
|
341
|
+
* @returns {boolean}
|
|
342
|
+
*/
|
|
343
|
+
function failFlow(state, error) {
|
|
344
|
+
return updateFlow(state, { status: 'failed', error });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Cancel and remove a flow
|
|
349
|
+
* @param {string} state
|
|
350
|
+
* @returns {boolean}
|
|
351
|
+
*/
|
|
352
|
+
function cancelFlow(state) {
|
|
353
|
+
return pendingFlows.delete(state);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
module.exports = {
|
|
357
|
+
// PKCE
|
|
358
|
+
generatePKCE,
|
|
359
|
+
|
|
360
|
+
// State
|
|
361
|
+
generateState,
|
|
362
|
+
cleanupExpiredFlows,
|
|
363
|
+
|
|
364
|
+
// Auth URL
|
|
365
|
+
buildAuthUrl,
|
|
366
|
+
|
|
367
|
+
// Token operations
|
|
368
|
+
exchangeCodeForToken,
|
|
369
|
+
refreshToken,
|
|
370
|
+
|
|
371
|
+
// Flow management
|
|
372
|
+
startFlow,
|
|
373
|
+
getPendingFlow,
|
|
374
|
+
updateFlow,
|
|
375
|
+
completeFlow,
|
|
376
|
+
failFlow,
|
|
377
|
+
cancelFlow
|
|
378
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000; // 5 minutes
|
|
6
|
+
|
|
7
|
+
function getTokensFilePath() {
|
|
8
|
+
const dir = path.join(os.homedir(), '.claude', 'cc-tool');
|
|
9
|
+
if (!fs.existsSync(dir)) {
|
|
10
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
return path.join(dir, 'oauth-tokens.json');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function loadTokens() {
|
|
16
|
+
const filePath = getTokensFilePath();
|
|
17
|
+
try {
|
|
18
|
+
if (fs.existsSync(filePath)) {
|
|
19
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
20
|
+
return JSON.parse(content);
|
|
21
|
+
}
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('Error loading oauth tokens:', error);
|
|
24
|
+
}
|
|
25
|
+
return { tokens: {} };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function saveTokens(data) {
|
|
29
|
+
const filePath = getTokensFilePath();
|
|
30
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
31
|
+
|
|
32
|
+
// Set file permissions to 0600 (owner read/write only) on non-Windows
|
|
33
|
+
if (process.platform !== 'win32') {
|
|
34
|
+
try {
|
|
35
|
+
fs.chmodSync(filePath, 0o600);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('Error setting oauth tokens file permissions:', error);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getToken(tokenId) {
|
|
43
|
+
const data = loadTokens();
|
|
44
|
+
return data.tokens[tokenId] || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function saveToken(tokenData) {
|
|
48
|
+
const data = loadTokens();
|
|
49
|
+
const id = tokenData.id || `token-${Date.now()}`;
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
|
|
52
|
+
data.tokens[id] = {
|
|
53
|
+
...tokenData,
|
|
54
|
+
id,
|
|
55
|
+
createdAt: tokenData.createdAt || now,
|
|
56
|
+
updatedAt: now
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
saveTokens(data);
|
|
60
|
+
return data.tokens[id];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function updateToken(tokenId, updates) {
|
|
64
|
+
const data = loadTokens();
|
|
65
|
+
|
|
66
|
+
if (!data.tokens[tokenId]) {
|
|
67
|
+
throw new Error('Token not found');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
data.tokens[tokenId] = {
|
|
71
|
+
...data.tokens[tokenId],
|
|
72
|
+
...updates,
|
|
73
|
+
id: tokenId, // prevent id override
|
|
74
|
+
updatedAt: Date.now()
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
saveTokens(data);
|
|
78
|
+
return data.tokens[tokenId];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function deleteToken(tokenId) {
|
|
82
|
+
const data = loadTokens();
|
|
83
|
+
|
|
84
|
+
if (!data.tokens[tokenId]) {
|
|
85
|
+
throw new Error('Token not found');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
delete data.tokens[tokenId];
|
|
89
|
+
saveTokens(data);
|
|
90
|
+
return { success: true };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function maskSensitiveField(value) {
|
|
94
|
+
if (!value || typeof value !== 'string') {
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
if (value.length <= 8) {
|
|
98
|
+
return '***';
|
|
99
|
+
}
|
|
100
|
+
return value.substring(0, 8) + '...';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getAllTokens() {
|
|
104
|
+
const data = loadTokens();
|
|
105
|
+
return Object.values(data.tokens).map(token => ({
|
|
106
|
+
...token,
|
|
107
|
+
accessToken: maskSensitiveField(token.accessToken),
|
|
108
|
+
refreshToken: maskSensitiveField(token.refreshToken)
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getTokensByProvider(provider) {
|
|
113
|
+
const all = getAllTokens();
|
|
114
|
+
return all.filter(token => token.provider === provider);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isTokenExpired(token) {
|
|
118
|
+
if (!token || !token.expiresAt) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
return Date.now() >= (token.expiresAt - TOKEN_EXPIRY_BUFFER_MS);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
loadTokens,
|
|
126
|
+
saveTokens,
|
|
127
|
+
getToken,
|
|
128
|
+
saveToken,
|
|
129
|
+
updateToken,
|
|
130
|
+
deleteToken,
|
|
131
|
+
getAllTokens,
|
|
132
|
+
getTokensByProvider,
|
|
133
|
+
isTokenExpired,
|
|
134
|
+
getTokensFilePath
|
|
135
|
+
};
|