octocode-shared 1.0.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/credentials/index.d.ts +6 -0
- package/dist/credentials/index.d.ts.map +1 -0
- package/dist/credentials/index.js +42 -0
- package/dist/credentials/keychain.d.ts +52 -0
- package/dist/credentials/keychain.d.ts.map +1 -0
- package/dist/credentials/storage.d.ts +367 -0
- package/dist/credentials/storage.d.ts.map +1 -0
- package/dist/credentials/types.d.ts +61 -0
- package/dist/credentials/types.d.ts.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +67 -0
- package/dist/platform/index.d.ts +5 -0
- package/dist/platform/index.d.ts.map +1 -0
- package/dist/platform/index.js +11 -0
- package/dist/platform/platform.d.ts +30 -0
- package/dist/platform/platform.d.ts.map +1 -0
- package/dist/platform-1V_81nPi.js +37 -0
- package/dist/session/index.d.ts +12 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +18 -0
- package/dist/session/storage.d.ts +85 -0
- package/dist/session/storage.d.ts.map +1 -0
- package/dist/session/types.d.ts +44 -0
- package/dist/session/types.d.ts.map +1 -0
- package/dist/storage-D-QEqQEn.js +244 -0
- package/dist/storage-DuH3rTiu.js +737 -0
- package/package.json +82 -0
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { randomBytes, createCipheriv, createDecipheriv } from "node:crypto";
|
|
4
|
+
import { refreshToken } from "@octokit/oauth-methods";
|
|
5
|
+
import { request } from "@octokit/request";
|
|
6
|
+
import { H as HOME } from "./platform-1V_81nPi.js";
|
|
7
|
+
import { AsyncEntry, findCredentialsAsync } from "@napi-rs/keyring";
|
|
8
|
+
function isKeychainAvailable() {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
async function setPassword(service, account, password) {
|
|
12
|
+
const entry = new AsyncEntry(service, account);
|
|
13
|
+
await entry.setPassword(password);
|
|
14
|
+
}
|
|
15
|
+
async function getPassword(service, account) {
|
|
16
|
+
try {
|
|
17
|
+
const entry = new AsyncEntry(service, account);
|
|
18
|
+
const result = await entry.getPassword();
|
|
19
|
+
return result ?? null;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function deletePassword(service, account) {
|
|
25
|
+
try {
|
|
26
|
+
const entry = new AsyncEntry(service, account);
|
|
27
|
+
return await entry.deleteCredential();
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function findCredentials(service) {
|
|
33
|
+
try {
|
|
34
|
+
const credentials = await findCredentialsAsync(service);
|
|
35
|
+
return credentials;
|
|
36
|
+
} catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const DEFAULT_CLIENT_ID = "178c6fc778ccc68e1d6a";
|
|
41
|
+
const DEFAULT_HOSTNAME = "github.com";
|
|
42
|
+
const KEYRING_TIMEOUT_MS = 3e3;
|
|
43
|
+
class TimeoutError extends Error {
|
|
44
|
+
constructor(message) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = "TimeoutError";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function withTimeout(promise, ms) {
|
|
50
|
+
return Promise.race([
|
|
51
|
+
promise,
|
|
52
|
+
new Promise(
|
|
53
|
+
(_, reject) => setTimeout(
|
|
54
|
+
() => reject(new TimeoutError(`Operation timed out after ${ms}ms`)),
|
|
55
|
+
ms
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
]);
|
|
59
|
+
}
|
|
60
|
+
let _keychainAvailable = null;
|
|
61
|
+
function checkKeychainAvailable() {
|
|
62
|
+
if (_keychainAvailable === null) {
|
|
63
|
+
_keychainAvailable = isKeychainAvailable();
|
|
64
|
+
}
|
|
65
|
+
return _keychainAvailable;
|
|
66
|
+
}
|
|
67
|
+
const KEYCHAIN_SERVICE = "octocode-cli";
|
|
68
|
+
const OCTOCODE_DIR = join(HOME, ".octocode");
|
|
69
|
+
const CREDENTIALS_FILE = join(OCTOCODE_DIR, "credentials.json");
|
|
70
|
+
const KEY_FILE = join(OCTOCODE_DIR, ".key");
|
|
71
|
+
const ALGORITHM = "aes-256-gcm";
|
|
72
|
+
const IV_LENGTH = 16;
|
|
73
|
+
const credentialsCache = /* @__PURE__ */ new Map();
|
|
74
|
+
const CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
75
|
+
function isCacheValid(hostname) {
|
|
76
|
+
const cached = credentialsCache.get(hostname);
|
|
77
|
+
if (!cached) return false;
|
|
78
|
+
const age = Date.now() - cached.cachedAt;
|
|
79
|
+
return age < CACHE_TTL_MS;
|
|
80
|
+
}
|
|
81
|
+
function invalidateCredentialsCache(hostname) {
|
|
82
|
+
if (hostname) {
|
|
83
|
+
credentialsCache.delete(normalizeHostname(hostname));
|
|
84
|
+
} else {
|
|
85
|
+
credentialsCache.clear();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function _getCacheStats() {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
return {
|
|
91
|
+
size: credentialsCache.size,
|
|
92
|
+
entries: Array.from(credentialsCache.entries()).map(
|
|
93
|
+
([hostname, entry]) => ({
|
|
94
|
+
hostname,
|
|
95
|
+
age: now - entry.cachedAt,
|
|
96
|
+
valid: isCacheValid(hostname)
|
|
97
|
+
})
|
|
98
|
+
)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function _resetCredentialsCache() {
|
|
102
|
+
credentialsCache.clear();
|
|
103
|
+
}
|
|
104
|
+
const ENV_TOKEN_VARS = [
|
|
105
|
+
"OCTOCODE_TOKEN",
|
|
106
|
+
// octocode-specific (highest priority)
|
|
107
|
+
"GH_TOKEN",
|
|
108
|
+
// gh CLI compatible
|
|
109
|
+
"GITHUB_TOKEN"
|
|
110
|
+
// GitHub Actions native
|
|
111
|
+
];
|
|
112
|
+
function getTokenFromEnv() {
|
|
113
|
+
for (const envVar of ENV_TOKEN_VARS) {
|
|
114
|
+
const token = process.env[envVar];
|
|
115
|
+
if (token && token.trim()) {
|
|
116
|
+
return token.trim();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
function getEnvTokenSource() {
|
|
122
|
+
for (const envVar of ENV_TOKEN_VARS) {
|
|
123
|
+
const token = process.env[envVar];
|
|
124
|
+
if (token && token.trim()) {
|
|
125
|
+
return `env:${envVar}`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
function hasEnvToken() {
|
|
131
|
+
return getTokenFromEnv() !== null;
|
|
132
|
+
}
|
|
133
|
+
let _useSecureStorage = null;
|
|
134
|
+
let _keychainInitialized = false;
|
|
135
|
+
async function initializeSecureStorage() {
|
|
136
|
+
if (_keychainInitialized) {
|
|
137
|
+
return _useSecureStorage ?? false;
|
|
138
|
+
}
|
|
139
|
+
_keychainInitialized = true;
|
|
140
|
+
_useSecureStorage = checkKeychainAvailable();
|
|
141
|
+
return _useSecureStorage;
|
|
142
|
+
}
|
|
143
|
+
function isSecureStorageAvailable() {
|
|
144
|
+
if (_useSecureStorage !== null) {
|
|
145
|
+
return _useSecureStorage;
|
|
146
|
+
}
|
|
147
|
+
_useSecureStorage = checkKeychainAvailable();
|
|
148
|
+
return _useSecureStorage;
|
|
149
|
+
}
|
|
150
|
+
function _setSecureStorageAvailable(available) {
|
|
151
|
+
_useSecureStorage = available;
|
|
152
|
+
_keychainAvailable = available;
|
|
153
|
+
_keychainInitialized = true;
|
|
154
|
+
}
|
|
155
|
+
function _resetSecureStorageState() {
|
|
156
|
+
_useSecureStorage = null;
|
|
157
|
+
_keychainAvailable = null;
|
|
158
|
+
_keychainInitialized = false;
|
|
159
|
+
}
|
|
160
|
+
async function keychainStore(hostname, credentials) {
|
|
161
|
+
if (!checkKeychainAvailable()) {
|
|
162
|
+
throw new Error("Keychain not available");
|
|
163
|
+
}
|
|
164
|
+
const data = JSON.stringify(credentials);
|
|
165
|
+
await setPassword(KEYCHAIN_SERVICE, hostname, data);
|
|
166
|
+
}
|
|
167
|
+
async function keychainGet(hostname) {
|
|
168
|
+
if (!checkKeychainAvailable()) return null;
|
|
169
|
+
try {
|
|
170
|
+
const data = await getPassword(KEYCHAIN_SERVICE, hostname);
|
|
171
|
+
if (!data) return null;
|
|
172
|
+
return JSON.parse(data);
|
|
173
|
+
} catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function keychainDelete(hostname) {
|
|
178
|
+
if (!checkKeychainAvailable()) return false;
|
|
179
|
+
try {
|
|
180
|
+
return await deletePassword(KEYCHAIN_SERVICE, hostname);
|
|
181
|
+
} catch {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function keychainList() {
|
|
186
|
+
if (!checkKeychainAvailable()) return [];
|
|
187
|
+
try {
|
|
188
|
+
const credentials = await findCredentials(KEYCHAIN_SERVICE);
|
|
189
|
+
return credentials.map((c) => c.account);
|
|
190
|
+
} catch {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function getOrCreateKey() {
|
|
195
|
+
ensureOctocodeDir();
|
|
196
|
+
if (existsSync(KEY_FILE)) {
|
|
197
|
+
return Buffer.from(readFileSync(KEY_FILE, "utf8"), "hex");
|
|
198
|
+
}
|
|
199
|
+
const key = randomBytes(32);
|
|
200
|
+
writeFileSync(KEY_FILE, key.toString("hex"), { mode: 384 });
|
|
201
|
+
return key;
|
|
202
|
+
}
|
|
203
|
+
function encrypt(data) {
|
|
204
|
+
const key = getOrCreateKey();
|
|
205
|
+
const iv = randomBytes(IV_LENGTH);
|
|
206
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
207
|
+
let encrypted = cipher.update(data, "utf8", "hex");
|
|
208
|
+
encrypted += cipher.final("hex");
|
|
209
|
+
const authTag = cipher.getAuthTag();
|
|
210
|
+
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
|
|
211
|
+
}
|
|
212
|
+
function decrypt(encryptedData) {
|
|
213
|
+
const key = getOrCreateKey();
|
|
214
|
+
const [ivHex, authTagHex, encrypted] = encryptedData.split(":");
|
|
215
|
+
if (!ivHex || !authTagHex || !encrypted) {
|
|
216
|
+
throw new Error("Invalid encrypted data format");
|
|
217
|
+
}
|
|
218
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
219
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
220
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
221
|
+
decipher.setAuthTag(authTag);
|
|
222
|
+
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
|
223
|
+
decrypted += decipher.final("utf8");
|
|
224
|
+
return decrypted;
|
|
225
|
+
}
|
|
226
|
+
function ensureOctocodeDir() {
|
|
227
|
+
if (!existsSync(OCTOCODE_DIR)) {
|
|
228
|
+
mkdirSync(OCTOCODE_DIR, { recursive: true, mode: 448 });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function readCredentialsStore() {
|
|
232
|
+
ensureOctocodeDir();
|
|
233
|
+
if (!existsSync(CREDENTIALS_FILE)) {
|
|
234
|
+
return { version: 1, credentials: {} };
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
const encryptedContent = readFileSync(CREDENTIALS_FILE, "utf8");
|
|
238
|
+
const decrypted = decrypt(encryptedContent);
|
|
239
|
+
return JSON.parse(decrypted);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.error(
|
|
242
|
+
"\n ⚠ Warning: Could not read credentials file. You may need to login again."
|
|
243
|
+
);
|
|
244
|
+
console.error(` File: ${CREDENTIALS_FILE}`);
|
|
245
|
+
if (error instanceof Error && error.message) {
|
|
246
|
+
console.error(` Reason: ${error.message}
|
|
247
|
+
`);
|
|
248
|
+
}
|
|
249
|
+
return { version: 1, credentials: {} };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function writeCredentialsStore(store) {
|
|
253
|
+
ensureOctocodeDir();
|
|
254
|
+
const encrypted = encrypt(JSON.stringify(store, null, 2));
|
|
255
|
+
writeFileSync(CREDENTIALS_FILE, encrypted, { mode: 384 });
|
|
256
|
+
}
|
|
257
|
+
function removeFromFileStorage(hostname) {
|
|
258
|
+
try {
|
|
259
|
+
const store = readCredentialsStore();
|
|
260
|
+
if (store.credentials[hostname]) {
|
|
261
|
+
delete store.credentials[hostname];
|
|
262
|
+
if (Object.keys(store.credentials).length === 0) {
|
|
263
|
+
cleanupKeyFile();
|
|
264
|
+
} else {
|
|
265
|
+
writeCredentialsStore(store);
|
|
266
|
+
}
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
return false;
|
|
270
|
+
} catch (err) {
|
|
271
|
+
console.warn(
|
|
272
|
+
`[token-storage] Failed to remove from file storage: ${err instanceof Error ? err.message : String(err)}`
|
|
273
|
+
);
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function cleanupKeyFile() {
|
|
278
|
+
try {
|
|
279
|
+
if (existsSync(CREDENTIALS_FILE)) {
|
|
280
|
+
unlinkSync(CREDENTIALS_FILE);
|
|
281
|
+
}
|
|
282
|
+
if (existsSync(KEY_FILE)) {
|
|
283
|
+
unlinkSync(KEY_FILE);
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async function migrateSingleCredential(hostname, credentials) {
|
|
289
|
+
try {
|
|
290
|
+
await withTimeout(keychainStore(hostname, credentials), KEYRING_TIMEOUT_MS);
|
|
291
|
+
removeFromFileStorage(hostname);
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function normalizeHostname(hostname) {
|
|
296
|
+
return hostname.toLowerCase().replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
297
|
+
}
|
|
298
|
+
async function storeCredentials(credentials) {
|
|
299
|
+
const hostname = normalizeHostname(credentials.hostname);
|
|
300
|
+
const normalizedCredentials = {
|
|
301
|
+
...credentials,
|
|
302
|
+
hostname,
|
|
303
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
304
|
+
};
|
|
305
|
+
if (isSecureStorageAvailable()) {
|
|
306
|
+
try {
|
|
307
|
+
await withTimeout(
|
|
308
|
+
keychainStore(hostname, normalizedCredentials),
|
|
309
|
+
KEYRING_TIMEOUT_MS
|
|
310
|
+
);
|
|
311
|
+
removeFromFileStorage(hostname);
|
|
312
|
+
invalidateCredentialsCache(hostname);
|
|
313
|
+
return { success: true, insecureStorageUsed: false };
|
|
314
|
+
} catch (err) {
|
|
315
|
+
console.warn(
|
|
316
|
+
`[token-storage] Keyring storage failed, using file fallback: ${err instanceof Error ? err.message : String(err)}`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
const store = readCredentialsStore();
|
|
322
|
+
store.credentials[hostname] = normalizedCredentials;
|
|
323
|
+
writeCredentialsStore(store);
|
|
324
|
+
invalidateCredentialsCache(hostname);
|
|
325
|
+
return { success: true, insecureStorageUsed: true };
|
|
326
|
+
} catch (fileError) {
|
|
327
|
+
console.error(`[token-storage] CRITICAL: All storage methods failed!`);
|
|
328
|
+
console.error(
|
|
329
|
+
` Error: ${fileError instanceof Error ? fileError.message : String(fileError)}`
|
|
330
|
+
);
|
|
331
|
+
throw new Error("Failed to store credentials");
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async function getCredentials(hostname = "github.com", options) {
|
|
335
|
+
const normalizedHostname = normalizeHostname(hostname);
|
|
336
|
+
if (!options?.bypassCache && isCacheValid(normalizedHostname)) {
|
|
337
|
+
return credentialsCache.get(normalizedHostname).credentials;
|
|
338
|
+
}
|
|
339
|
+
const credentials = await fetchCredentialsFromStorage(normalizedHostname);
|
|
340
|
+
if (credentials) {
|
|
341
|
+
credentialsCache.set(normalizedHostname, {
|
|
342
|
+
credentials,
|
|
343
|
+
cachedAt: Date.now()
|
|
344
|
+
});
|
|
345
|
+
} else {
|
|
346
|
+
credentialsCache.delete(normalizedHostname);
|
|
347
|
+
}
|
|
348
|
+
return credentials;
|
|
349
|
+
}
|
|
350
|
+
async function fetchCredentialsFromStorage(normalizedHostname) {
|
|
351
|
+
if (isSecureStorageAvailable()) {
|
|
352
|
+
try {
|
|
353
|
+
const creds = await withTimeout(
|
|
354
|
+
keychainGet(normalizedHostname),
|
|
355
|
+
KEYRING_TIMEOUT_MS
|
|
356
|
+
);
|
|
357
|
+
if (creds) return creds;
|
|
358
|
+
} catch (err) {
|
|
359
|
+
if (!(err instanceof TimeoutError)) {
|
|
360
|
+
console.warn(
|
|
361
|
+
`[token-storage] Keyring read failed: ${err instanceof Error ? err.message : String(err)}`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const store = readCredentialsStore();
|
|
367
|
+
const fileCreds = store.credentials[normalizedHostname];
|
|
368
|
+
if (fileCreds) {
|
|
369
|
+
if (isSecureStorageAvailable()) {
|
|
370
|
+
migrateSingleCredential(normalizedHostname, fileCreds).catch(() => {
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
return fileCreds;
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
function getCredentialsSync(hostname = "github.com") {
|
|
378
|
+
const normalizedHostname = normalizeHostname(hostname);
|
|
379
|
+
const store = readCredentialsStore();
|
|
380
|
+
return store.credentials[normalizedHostname] || null;
|
|
381
|
+
}
|
|
382
|
+
async function deleteCredentials(hostname = "github.com") {
|
|
383
|
+
const normalizedHostname = normalizeHostname(hostname);
|
|
384
|
+
let deletedFromKeyring = false;
|
|
385
|
+
let deletedFromFile = false;
|
|
386
|
+
if (isSecureStorageAvailable()) {
|
|
387
|
+
try {
|
|
388
|
+
deletedFromKeyring = await withTimeout(
|
|
389
|
+
keychainDelete(normalizedHostname),
|
|
390
|
+
KEYRING_TIMEOUT_MS
|
|
391
|
+
);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
console.warn(
|
|
394
|
+
`[token-storage] Keyring delete failed: ${err instanceof Error ? err.message : String(err)}`
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const store = readCredentialsStore();
|
|
399
|
+
if (store.credentials[normalizedHostname]) {
|
|
400
|
+
delete store.credentials[normalizedHostname];
|
|
401
|
+
if (Object.keys(store.credentials).length === 0) {
|
|
402
|
+
cleanupKeyFile();
|
|
403
|
+
} else {
|
|
404
|
+
writeCredentialsStore(store);
|
|
405
|
+
}
|
|
406
|
+
deletedFromFile = true;
|
|
407
|
+
}
|
|
408
|
+
invalidateCredentialsCache(normalizedHostname);
|
|
409
|
+
return {
|
|
410
|
+
success: deletedFromKeyring || deletedFromFile,
|
|
411
|
+
deletedFromKeyring,
|
|
412
|
+
deletedFromFile
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
async function listStoredHosts() {
|
|
416
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
417
|
+
if (isSecureStorageAvailable()) {
|
|
418
|
+
try {
|
|
419
|
+
const keychainHosts = await withTimeout(
|
|
420
|
+
keychainList(),
|
|
421
|
+
KEYRING_TIMEOUT_MS
|
|
422
|
+
);
|
|
423
|
+
keychainHosts.forEach((h) => hosts.add(h));
|
|
424
|
+
} catch (err) {
|
|
425
|
+
if (!(err instanceof TimeoutError)) {
|
|
426
|
+
console.warn(
|
|
427
|
+
`[token-storage] Keyring list failed: ${err instanceof Error ? err.message : String(err)}`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const store = readCredentialsStore();
|
|
433
|
+
Object.keys(store.credentials).forEach((h) => hosts.add(h));
|
|
434
|
+
return Array.from(hosts);
|
|
435
|
+
}
|
|
436
|
+
function listStoredHostsSync() {
|
|
437
|
+
const store = readCredentialsStore();
|
|
438
|
+
return Object.keys(store.credentials);
|
|
439
|
+
}
|
|
440
|
+
async function hasCredentials(hostname = "github.com") {
|
|
441
|
+
return await getCredentials(hostname) !== null;
|
|
442
|
+
}
|
|
443
|
+
function hasCredentialsSync(hostname = "github.com") {
|
|
444
|
+
return getCredentialsSync(hostname) !== null;
|
|
445
|
+
}
|
|
446
|
+
async function updateToken(hostname, token) {
|
|
447
|
+
const credentials = await getCredentials(hostname);
|
|
448
|
+
if (!credentials) {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
credentials.token = token;
|
|
452
|
+
credentials.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
453
|
+
await storeCredentials(credentials);
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
function getCredentialsFilePath() {
|
|
457
|
+
if (isSecureStorageAvailable()) {
|
|
458
|
+
return "System Keychain (secure)";
|
|
459
|
+
}
|
|
460
|
+
return CREDENTIALS_FILE;
|
|
461
|
+
}
|
|
462
|
+
function isUsingSecureStorage() {
|
|
463
|
+
return isSecureStorageAvailable();
|
|
464
|
+
}
|
|
465
|
+
function isTokenExpired(credentials) {
|
|
466
|
+
if (!credentials.token.expiresAt) {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
const expiresAt = new Date(credentials.token.expiresAt);
|
|
470
|
+
if (isNaN(expiresAt.getTime())) {
|
|
471
|
+
return true;
|
|
472
|
+
}
|
|
473
|
+
const now = /* @__PURE__ */ new Date();
|
|
474
|
+
return expiresAt.getTime() - now.getTime() < 5 * 60 * 1e3;
|
|
475
|
+
}
|
|
476
|
+
function isRefreshTokenExpired(credentials) {
|
|
477
|
+
if (!credentials.token.refreshTokenExpiresAt) {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
const expiresAt = new Date(credentials.token.refreshTokenExpiresAt);
|
|
481
|
+
if (isNaN(expiresAt.getTime())) {
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
return /* @__PURE__ */ new Date() >= expiresAt;
|
|
485
|
+
}
|
|
486
|
+
async function getToken(hostname = "github.com") {
|
|
487
|
+
const credentials = await getCredentials(hostname);
|
|
488
|
+
if (!credentials || !credentials.token) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
if (isTokenExpired(credentials)) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
return credentials.token.token;
|
|
495
|
+
}
|
|
496
|
+
function getApiBaseUrl(hostname) {
|
|
497
|
+
if (hostname === "github.com" || hostname === DEFAULT_HOSTNAME) {
|
|
498
|
+
return "https://api.github.com";
|
|
499
|
+
}
|
|
500
|
+
return `https://${hostname}/api/v3`;
|
|
501
|
+
}
|
|
502
|
+
async function refreshAuthToken(hostname = DEFAULT_HOSTNAME, clientId = DEFAULT_CLIENT_ID) {
|
|
503
|
+
const credentials = await getCredentials(hostname);
|
|
504
|
+
if (!credentials) {
|
|
505
|
+
return {
|
|
506
|
+
success: false,
|
|
507
|
+
error: `Not logged in to ${hostname}`
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
if (!credentials.token.refreshToken) {
|
|
511
|
+
return {
|
|
512
|
+
success: false,
|
|
513
|
+
error: "Token does not support refresh (OAuth App tokens do not expire)"
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
if (isRefreshTokenExpired(credentials)) {
|
|
517
|
+
return {
|
|
518
|
+
success: false,
|
|
519
|
+
error: "Refresh token has expired. Please login again."
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
const response = await refreshToken({
|
|
524
|
+
clientType: "github-app",
|
|
525
|
+
clientId,
|
|
526
|
+
clientSecret: "",
|
|
527
|
+
// Empty for OAuth apps
|
|
528
|
+
refreshToken: credentials.token.refreshToken,
|
|
529
|
+
request: request.defaults({
|
|
530
|
+
baseUrl: getApiBaseUrl(hostname)
|
|
531
|
+
})
|
|
532
|
+
});
|
|
533
|
+
const newToken = {
|
|
534
|
+
token: response.authentication.token,
|
|
535
|
+
tokenType: "oauth",
|
|
536
|
+
refreshToken: response.authentication.refreshToken,
|
|
537
|
+
expiresAt: response.authentication.expiresAt,
|
|
538
|
+
refreshTokenExpiresAt: response.authentication.refreshTokenExpiresAt
|
|
539
|
+
};
|
|
540
|
+
await updateToken(hostname, newToken);
|
|
541
|
+
return {
|
|
542
|
+
success: true,
|
|
543
|
+
username: credentials.username,
|
|
544
|
+
hostname
|
|
545
|
+
};
|
|
546
|
+
} catch (error) {
|
|
547
|
+
return {
|
|
548
|
+
success: false,
|
|
549
|
+
error: error instanceof Error ? error.message : "Token refresh failed"
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
async function getTokenWithRefresh(hostname = DEFAULT_HOSTNAME, clientId = DEFAULT_CLIENT_ID) {
|
|
554
|
+
const credentials = await getCredentials(hostname);
|
|
555
|
+
if (!credentials || !credentials.token) {
|
|
556
|
+
return { token: null, source: "none" };
|
|
557
|
+
}
|
|
558
|
+
if (!isTokenExpired(credentials)) {
|
|
559
|
+
return {
|
|
560
|
+
token: credentials.token.token,
|
|
561
|
+
source: "stored",
|
|
562
|
+
username: credentials.username
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
if (credentials.token.refreshToken) {
|
|
566
|
+
const refreshResult = await refreshAuthToken(hostname, clientId);
|
|
567
|
+
if (refreshResult.success) {
|
|
568
|
+
const updatedCredentials = await getCredentials(hostname);
|
|
569
|
+
if (updatedCredentials?.token.token) {
|
|
570
|
+
return {
|
|
571
|
+
token: updatedCredentials.token.token,
|
|
572
|
+
source: "refreshed",
|
|
573
|
+
username: updatedCredentials.username
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
token: null,
|
|
579
|
+
source: "none",
|
|
580
|
+
refreshError: refreshResult.error
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
return {
|
|
584
|
+
token: null,
|
|
585
|
+
source: "none",
|
|
586
|
+
refreshError: "Token expired and no refresh token available"
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
async function resolveToken(hostname = "github.com") {
|
|
590
|
+
const envToken = getTokenFromEnv();
|
|
591
|
+
if (envToken) {
|
|
592
|
+
return {
|
|
593
|
+
token: envToken,
|
|
594
|
+
source: getEnvTokenSource() ?? "env:GITHUB_TOKEN"
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
const storedToken = await getToken(hostname);
|
|
598
|
+
if (storedToken) {
|
|
599
|
+
const source = isSecureStorageAvailable() ? "keychain" : "file";
|
|
600
|
+
return {
|
|
601
|
+
token: storedToken,
|
|
602
|
+
source
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
async function resolveTokenWithRefresh(hostname = DEFAULT_HOSTNAME, clientId = DEFAULT_CLIENT_ID) {
|
|
608
|
+
const envToken = getTokenFromEnv();
|
|
609
|
+
if (envToken) {
|
|
610
|
+
return {
|
|
611
|
+
token: envToken,
|
|
612
|
+
source: getEnvTokenSource() ?? "env:GITHUB_TOKEN",
|
|
613
|
+
wasRefreshed: false
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
const result = await getTokenWithRefresh(hostname, clientId);
|
|
617
|
+
if (result.token) {
|
|
618
|
+
const source = isSecureStorageAvailable() ? "keychain" : "file";
|
|
619
|
+
return {
|
|
620
|
+
token: result.token,
|
|
621
|
+
source,
|
|
622
|
+
wasRefreshed: result.source === "refreshed",
|
|
623
|
+
username: result.username
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
if (result.refreshError) {
|
|
627
|
+
return {
|
|
628
|
+
token: "",
|
|
629
|
+
source: null,
|
|
630
|
+
wasRefreshed: false,
|
|
631
|
+
refreshError: result.refreshError
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
async function resolveTokenFull(options) {
|
|
637
|
+
const hostname = options?.hostname ?? DEFAULT_HOSTNAME;
|
|
638
|
+
const clientId = options?.clientId ?? DEFAULT_CLIENT_ID;
|
|
639
|
+
const getGhCliToken = options?.getGhCliToken;
|
|
640
|
+
const envToken = getTokenFromEnv();
|
|
641
|
+
if (envToken) {
|
|
642
|
+
return {
|
|
643
|
+
token: envToken,
|
|
644
|
+
source: getEnvTokenSource() ?? "env:GITHUB_TOKEN",
|
|
645
|
+
wasRefreshed: false
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
return resolveTokenFullInternalNoEnv(hostname, clientId, getGhCliToken);
|
|
649
|
+
}
|
|
650
|
+
async function resolveTokenFullInternalNoEnv(hostname, clientId, getGhCliToken) {
|
|
651
|
+
const result = await getTokenWithRefresh(hostname, clientId);
|
|
652
|
+
if (result.token) {
|
|
653
|
+
const source = isSecureStorageAvailable() ? "keychain" : "file";
|
|
654
|
+
return {
|
|
655
|
+
token: result.token,
|
|
656
|
+
source,
|
|
657
|
+
wasRefreshed: result.source === "refreshed",
|
|
658
|
+
username: result.username
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
const refreshError = result.refreshError;
|
|
662
|
+
if (getGhCliToken) {
|
|
663
|
+
try {
|
|
664
|
+
const ghToken = await Promise.resolve(getGhCliToken(hostname));
|
|
665
|
+
if (ghToken?.trim()) {
|
|
666
|
+
return {
|
|
667
|
+
token: ghToken.trim(),
|
|
668
|
+
source: "gh-cli",
|
|
669
|
+
wasRefreshed: false,
|
|
670
|
+
refreshError
|
|
671
|
+
// Include any refresh error from step 4-5
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
} catch {
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (refreshError) {
|
|
678
|
+
return {
|
|
679
|
+
token: "",
|
|
680
|
+
source: null,
|
|
681
|
+
wasRefreshed: false,
|
|
682
|
+
refreshError
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
function getTokenSync(hostname = "github.com") {
|
|
688
|
+
const credentials = getCredentialsSync(hostname);
|
|
689
|
+
if (!credentials || !credentials.token) {
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
if (isTokenExpired(credentials)) {
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
return credentials.token.token;
|
|
696
|
+
}
|
|
697
|
+
export {
|
|
698
|
+
ensureOctocodeDir as A,
|
|
699
|
+
getTokenFromEnv as B,
|
|
700
|
+
CREDENTIALS_FILE as C,
|
|
701
|
+
getEnvTokenSource as D,
|
|
702
|
+
ENV_TOKEN_VARS as E,
|
|
703
|
+
hasEnvToken as F,
|
|
704
|
+
_resetSecureStorageState as G,
|
|
705
|
+
_getCacheStats as H,
|
|
706
|
+
_resetCredentialsCache as I,
|
|
707
|
+
KEY_FILE as K,
|
|
708
|
+
OCTOCODE_DIR as O,
|
|
709
|
+
TimeoutError as T,
|
|
710
|
+
_setSecureStorageAvailable as _,
|
|
711
|
+
isSecureStorageAvailable as a,
|
|
712
|
+
isUsingSecureStorage as b,
|
|
713
|
+
getCredentialsSync as c,
|
|
714
|
+
deleteCredentials as d,
|
|
715
|
+
invalidateCredentialsCache as e,
|
|
716
|
+
getToken as f,
|
|
717
|
+
getCredentials as g,
|
|
718
|
+
getTokenSync as h,
|
|
719
|
+
initializeSecureStorage as i,
|
|
720
|
+
getTokenWithRefresh as j,
|
|
721
|
+
resolveTokenWithRefresh as k,
|
|
722
|
+
refreshAuthToken as l,
|
|
723
|
+
resolveTokenFull as m,
|
|
724
|
+
listStoredHosts as n,
|
|
725
|
+
listStoredHostsSync as o,
|
|
726
|
+
hasCredentials as p,
|
|
727
|
+
hasCredentialsSync as q,
|
|
728
|
+
resolveToken as r,
|
|
729
|
+
storeCredentials as s,
|
|
730
|
+
isTokenExpired as t,
|
|
731
|
+
updateToken as u,
|
|
732
|
+
isRefreshTokenExpired as v,
|
|
733
|
+
getCredentialsFilePath as w,
|
|
734
|
+
readCredentialsStore as x,
|
|
735
|
+
encrypt as y,
|
|
736
|
+
decrypt as z
|
|
737
|
+
};
|