@docyrus/cli 0.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/README.md +249 -0
- package/dist/cli.js +2925 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.js +793 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir, hostname, userInfo } from 'os';
|
|
4
|
+
import { randomBytes, createDecipheriv, createCipheriv, scryptSync, createHash } from 'crypto';
|
|
5
|
+
import { AuthenticationError as AuthenticationError$1, RestApiClient } from '@docyrus/api-client';
|
|
6
|
+
import { createServer } from 'http';
|
|
7
|
+
import { URL } from 'url';
|
|
8
|
+
import open from 'open';
|
|
9
|
+
import Conf from 'conf';
|
|
10
|
+
|
|
11
|
+
// src/storage/file-storage.ts
|
|
12
|
+
|
|
13
|
+
// src/config/constants.ts
|
|
14
|
+
var DOCYRUS_API_URL = "https://alpha-api.docyrus.com";
|
|
15
|
+
var OAUTH_CLIENT_ID = "90565525-8283-4881-82a9-8613eb82ae27";
|
|
16
|
+
var OAUTH_SCOPES = "offline_access Read.All Users.Read Users.Read.All DS.Read.All".split(" ");
|
|
17
|
+
var OAUTH_CALLBACK_PORT_MIN = 9876;
|
|
18
|
+
var OAUTH_CALLBACK_PORT_MAX = 9899;
|
|
19
|
+
var OAUTH_REDIRECT_URI = (port) => `http://localhost:${port}/callback`;
|
|
20
|
+
var OAUTH_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
21
|
+
var KEYCHAIN_SERVICE = "docyrus-cli";
|
|
22
|
+
var CONFIG_DIR = ".docyrus";
|
|
23
|
+
var CREDENTIALS_FILE = "credentials.enc";
|
|
24
|
+
var TOKEN_KEYS = {
|
|
25
|
+
ACCESS_TOKEN: "accessToken",
|
|
26
|
+
REFRESH_TOKEN: "refreshToken",
|
|
27
|
+
USER_EMAIL: "userEmail",
|
|
28
|
+
GITHUB_TOKEN: "githubToken"
|
|
29
|
+
// For private template access - persists after logout
|
|
30
|
+
};
|
|
31
|
+
var CLI_NAME = "docyrus";
|
|
32
|
+
var CLI_VERSION = "0.0.1";
|
|
33
|
+
|
|
34
|
+
// src/utils/platform.ts
|
|
35
|
+
function getHomeDir() {
|
|
36
|
+
return homedir();
|
|
37
|
+
}
|
|
38
|
+
function getConfigDir() {
|
|
39
|
+
return join(getHomeDir(), CONFIG_DIR);
|
|
40
|
+
}
|
|
41
|
+
var ALGORITHM = "aes-256-gcm";
|
|
42
|
+
var KEY_LENGTH = 32;
|
|
43
|
+
var IV_LENGTH = 16;
|
|
44
|
+
var AUTH_TAG_LENGTH = 16;
|
|
45
|
+
var SALT_LENGTH = 32;
|
|
46
|
+
function getMachineId() {
|
|
47
|
+
const parts = [
|
|
48
|
+
hostname(),
|
|
49
|
+
userInfo().username,
|
|
50
|
+
process.platform,
|
|
51
|
+
process.arch
|
|
52
|
+
];
|
|
53
|
+
return createHash("sha256").update(parts.join("-")).digest("hex");
|
|
54
|
+
}
|
|
55
|
+
function deriveKey(salt) {
|
|
56
|
+
const machineId = getMachineId();
|
|
57
|
+
return scryptSync(machineId, salt, KEY_LENGTH);
|
|
58
|
+
}
|
|
59
|
+
function encrypt(data) {
|
|
60
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
61
|
+
const key = deriveKey(salt);
|
|
62
|
+
const iv = randomBytes(IV_LENGTH);
|
|
63
|
+
const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
64
|
+
const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
|
|
65
|
+
const authTag = cipher.getAuthTag();
|
|
66
|
+
return [
|
|
67
|
+
salt.toString("base64"),
|
|
68
|
+
iv.toString("base64"),
|
|
69
|
+
authTag.toString("base64"),
|
|
70
|
+
encrypted.toString("base64")
|
|
71
|
+
].join(":");
|
|
72
|
+
}
|
|
73
|
+
function decrypt(encryptedData) {
|
|
74
|
+
const parts = encryptedData.split(":");
|
|
75
|
+
if (parts.length !== 4) {
|
|
76
|
+
throw new Error("Invalid encrypted data format");
|
|
77
|
+
}
|
|
78
|
+
const [
|
|
79
|
+
saltB64,
|
|
80
|
+
ivB64,
|
|
81
|
+
authTagB64,
|
|
82
|
+
dataB64
|
|
83
|
+
] = parts;
|
|
84
|
+
const salt = Buffer.from(saltB64, "base64");
|
|
85
|
+
const iv = Buffer.from(ivB64, "base64");
|
|
86
|
+
const authTag = Buffer.from(authTagB64, "base64");
|
|
87
|
+
const encrypted = Buffer.from(dataB64, "base64");
|
|
88
|
+
const key = deriveKey(salt);
|
|
89
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
90
|
+
decipher.setAuthTag(authTag);
|
|
91
|
+
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
92
|
+
return decrypted.toString("utf8");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/storage/file-storage.ts
|
|
96
|
+
var FileStorage = class {
|
|
97
|
+
filePath;
|
|
98
|
+
constructor() {
|
|
99
|
+
const configDir = getConfigDir();
|
|
100
|
+
this.filePath = join(configDir, CREDENTIALS_FILE);
|
|
101
|
+
}
|
|
102
|
+
ensureConfigDir() {
|
|
103
|
+
const configDir = getConfigDir();
|
|
104
|
+
if (!existsSync(configDir)) {
|
|
105
|
+
mkdirSync(configDir, { recursive: true, mode: 448 });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
readData() {
|
|
109
|
+
if (!existsSync(this.filePath)) {
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const encrypted = readFileSync(this.filePath, "utf8");
|
|
114
|
+
const decrypted = decrypt(encrypted);
|
|
115
|
+
return JSON.parse(decrypted);
|
|
116
|
+
} catch {
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
writeData(data) {
|
|
121
|
+
this.ensureConfigDir();
|
|
122
|
+
const json = JSON.stringify(data);
|
|
123
|
+
const encrypted = encrypt(json);
|
|
124
|
+
writeFileSync(this.filePath, encrypted, { mode: 384 });
|
|
125
|
+
}
|
|
126
|
+
async get(key) {
|
|
127
|
+
const data = this.readData();
|
|
128
|
+
return data[key] ?? null;
|
|
129
|
+
}
|
|
130
|
+
async set(key, value) {
|
|
131
|
+
const data = this.readData();
|
|
132
|
+
data[key] = value;
|
|
133
|
+
this.writeData(data);
|
|
134
|
+
}
|
|
135
|
+
async delete(key) {
|
|
136
|
+
const data = this.readData();
|
|
137
|
+
delete data[key];
|
|
138
|
+
if (Object.keys(data).length === 0) {
|
|
139
|
+
this.clear();
|
|
140
|
+
} else {
|
|
141
|
+
this.writeData(data);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async clear() {
|
|
145
|
+
if (existsSync(this.filePath)) {
|
|
146
|
+
unlinkSync(this.filePath);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async has(key) {
|
|
150
|
+
const data = this.readData();
|
|
151
|
+
return key in data;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// src/storage/keychain-storage.ts
|
|
156
|
+
var keytar = null;
|
|
157
|
+
async function getKeytar() {
|
|
158
|
+
if (keytar !== null) {
|
|
159
|
+
return keytar;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
keytar = await import('keytar');
|
|
163
|
+
return keytar;
|
|
164
|
+
} catch {
|
|
165
|
+
keytar = null;
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
var KeychainStorage = class {
|
|
170
|
+
service;
|
|
171
|
+
constructor(service = KEYCHAIN_SERVICE) {
|
|
172
|
+
this.service = service;
|
|
173
|
+
}
|
|
174
|
+
async isAvailable() {
|
|
175
|
+
const kt = await getKeytar();
|
|
176
|
+
if (!kt) return false;
|
|
177
|
+
try {
|
|
178
|
+
await kt.findCredentials(this.service);
|
|
179
|
+
return true;
|
|
180
|
+
} catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async get(key) {
|
|
185
|
+
const kt = await getKeytar();
|
|
186
|
+
if (!kt) return null;
|
|
187
|
+
try {
|
|
188
|
+
return await kt.getPassword(this.service, key);
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async set(key, value) {
|
|
194
|
+
const kt = await getKeytar();
|
|
195
|
+
if (!kt) return false;
|
|
196
|
+
try {
|
|
197
|
+
await kt.setPassword(this.service, key, value);
|
|
198
|
+
return true;
|
|
199
|
+
} catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async delete(key) {
|
|
204
|
+
const kt = await getKeytar();
|
|
205
|
+
if (!kt) return false;
|
|
206
|
+
try {
|
|
207
|
+
return await kt.deletePassword(this.service, key);
|
|
208
|
+
} catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async clear() {
|
|
213
|
+
const kt = await getKeytar();
|
|
214
|
+
if (!kt) return;
|
|
215
|
+
try {
|
|
216
|
+
const credentials = await kt.findCredentials(this.service);
|
|
217
|
+
for (const cred of credentials) {
|
|
218
|
+
await kt.deletePassword(this.service, cred.account);
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// src/storage/cli-token-manager.ts
|
|
226
|
+
var CliTokenManager = class {
|
|
227
|
+
keychain;
|
|
228
|
+
fileStorage;
|
|
229
|
+
keychainAvailable = null;
|
|
230
|
+
constructor() {
|
|
231
|
+
this.keychain = new KeychainStorage();
|
|
232
|
+
this.fileStorage = new FileStorage();
|
|
233
|
+
}
|
|
234
|
+
async useKeychain() {
|
|
235
|
+
if (this.keychainAvailable === null) {
|
|
236
|
+
this.keychainAvailable = await this.keychain.isAvailable();
|
|
237
|
+
}
|
|
238
|
+
return this.keychainAvailable;
|
|
239
|
+
}
|
|
240
|
+
async getToken() {
|
|
241
|
+
if (await this.useKeychain()) {
|
|
242
|
+
const token = await this.keychain.get(TOKEN_KEYS.ACCESS_TOKEN);
|
|
243
|
+
if (token) return token;
|
|
244
|
+
}
|
|
245
|
+
return await this.fileStorage.get(TOKEN_KEYS.ACCESS_TOKEN);
|
|
246
|
+
}
|
|
247
|
+
async setToken(token) {
|
|
248
|
+
if (await this.useKeychain()) {
|
|
249
|
+
const success = await this.keychain.set(TOKEN_KEYS.ACCESS_TOKEN, token);
|
|
250
|
+
if (success) return;
|
|
251
|
+
}
|
|
252
|
+
await this.fileStorage.set(TOKEN_KEYS.ACCESS_TOKEN, token);
|
|
253
|
+
}
|
|
254
|
+
async clearToken() {
|
|
255
|
+
await this.keychain.delete(TOKEN_KEYS.ACCESS_TOKEN);
|
|
256
|
+
await this.fileStorage.delete(TOKEN_KEYS.ACCESS_TOKEN);
|
|
257
|
+
}
|
|
258
|
+
async getRefreshToken() {
|
|
259
|
+
if (await this.useKeychain()) {
|
|
260
|
+
const token = await this.keychain.get(TOKEN_KEYS.REFRESH_TOKEN);
|
|
261
|
+
if (token) return token;
|
|
262
|
+
}
|
|
263
|
+
return await this.fileStorage.get(TOKEN_KEYS.REFRESH_TOKEN);
|
|
264
|
+
}
|
|
265
|
+
async setRefreshToken(token) {
|
|
266
|
+
if (await this.useKeychain()) {
|
|
267
|
+
const success = await this.keychain.set(TOKEN_KEYS.REFRESH_TOKEN, token);
|
|
268
|
+
if (success) return;
|
|
269
|
+
}
|
|
270
|
+
await this.fileStorage.set(TOKEN_KEYS.REFRESH_TOKEN, token);
|
|
271
|
+
}
|
|
272
|
+
async clearRefreshToken() {
|
|
273
|
+
await this.keychain.delete(TOKEN_KEYS.REFRESH_TOKEN);
|
|
274
|
+
await this.fileStorage.delete(TOKEN_KEYS.REFRESH_TOKEN);
|
|
275
|
+
}
|
|
276
|
+
async getUserEmail() {
|
|
277
|
+
if (await this.useKeychain()) {
|
|
278
|
+
const email = await this.keychain.get(TOKEN_KEYS.USER_EMAIL);
|
|
279
|
+
if (email) return email;
|
|
280
|
+
}
|
|
281
|
+
return await this.fileStorage.get(TOKEN_KEYS.USER_EMAIL);
|
|
282
|
+
}
|
|
283
|
+
async setUserEmail(email) {
|
|
284
|
+
if (await this.useKeychain()) {
|
|
285
|
+
const success = await this.keychain.set(TOKEN_KEYS.USER_EMAIL, email);
|
|
286
|
+
if (success) return;
|
|
287
|
+
}
|
|
288
|
+
await this.fileStorage.set(TOKEN_KEYS.USER_EMAIL, email);
|
|
289
|
+
}
|
|
290
|
+
// GitHub token for private template access (persists after logout)
|
|
291
|
+
async getGithubToken() {
|
|
292
|
+
if (await this.useKeychain()) {
|
|
293
|
+
const token = await this.keychain.get(TOKEN_KEYS.GITHUB_TOKEN);
|
|
294
|
+
if (token) return token;
|
|
295
|
+
}
|
|
296
|
+
return await this.fileStorage.get(TOKEN_KEYS.GITHUB_TOKEN);
|
|
297
|
+
}
|
|
298
|
+
async setGithubToken(token) {
|
|
299
|
+
if (await this.useKeychain()) {
|
|
300
|
+
const success = await this.keychain.set(TOKEN_KEYS.GITHUB_TOKEN, token);
|
|
301
|
+
if (success) return;
|
|
302
|
+
}
|
|
303
|
+
await this.fileStorage.set(TOKEN_KEYS.GITHUB_TOKEN, token);
|
|
304
|
+
}
|
|
305
|
+
async clearAll() {
|
|
306
|
+
const githubToken = await this.getGithubToken();
|
|
307
|
+
await this.keychain.clear();
|
|
308
|
+
await this.fileStorage.clear();
|
|
309
|
+
if (githubToken) {
|
|
310
|
+
await this.setGithubToken(githubToken);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async isLoggedIn() {
|
|
314
|
+
const token = await this.getToken();
|
|
315
|
+
return token !== null;
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
var instance = null;
|
|
319
|
+
function getTokenManager() {
|
|
320
|
+
if (!instance) {
|
|
321
|
+
instance = new CliTokenManager();
|
|
322
|
+
}
|
|
323
|
+
return instance;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/utils/errors.ts
|
|
327
|
+
var CliError = class extends Error {
|
|
328
|
+
exitCode;
|
|
329
|
+
suggestion;
|
|
330
|
+
constructor(message, exitCode = 1, suggestion) {
|
|
331
|
+
super(message);
|
|
332
|
+
this.name = "CliError";
|
|
333
|
+
this.exitCode = exitCode;
|
|
334
|
+
this.suggestion = suggestion;
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
var AuthenticationError = class extends CliError {
|
|
338
|
+
constructor(message = "Authentication failed", suggestion) {
|
|
339
|
+
super(message, 1, suggestion || "Please check your credentials and try again.");
|
|
340
|
+
this.name = "AuthenticationError";
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
var NetworkError = class extends CliError {
|
|
344
|
+
constructor(message = "Network request failed", suggestion) {
|
|
345
|
+
super(message, 1, suggestion || "Please check your internet connection and try again.");
|
|
346
|
+
this.name = "NetworkError";
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
var TimeoutError = class extends CliError {
|
|
350
|
+
constructor(message = "Operation timed out") {
|
|
351
|
+
super(message, 1, "Please try again.");
|
|
352
|
+
this.name = "TimeoutError";
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
var OAuthError = class extends CliError {
|
|
356
|
+
constructor(message, suggestion) {
|
|
357
|
+
super(message, 1, suggestion || "Please try again or use email/password login.");
|
|
358
|
+
this.name = "OAuthError";
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// src/auth/credential-auth.ts
|
|
363
|
+
function getApiClient() {
|
|
364
|
+
return new RestApiClient({ baseURL: DOCYRUS_API_URL });
|
|
365
|
+
}
|
|
366
|
+
async function loginWithCredentials(credentials) {
|
|
367
|
+
const client = getApiClient();
|
|
368
|
+
try {
|
|
369
|
+
const response = await client.post("/v1/auth/token", {
|
|
370
|
+
email: credentials.email,
|
|
371
|
+
password: credentials.password
|
|
372
|
+
});
|
|
373
|
+
const tokens = {
|
|
374
|
+
accessToken: response.data.access_token,
|
|
375
|
+
refreshToken: response.data.refresh_token,
|
|
376
|
+
expiresIn: response.data.expires_in
|
|
377
|
+
};
|
|
378
|
+
if (tokens.expiresIn) {
|
|
379
|
+
tokens.expiresAt = Date.now() + tokens.expiresIn * 1e3;
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
tokens,
|
|
383
|
+
user: response.data.user
|
|
384
|
+
};
|
|
385
|
+
} catch (error) {
|
|
386
|
+
if (error instanceof AuthenticationError$1) {
|
|
387
|
+
throw new AuthenticationError("Invalid email or password.");
|
|
388
|
+
}
|
|
389
|
+
if (error instanceof Error && error.message.includes("fetch")) {
|
|
390
|
+
throw new NetworkError();
|
|
391
|
+
}
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async function getUserInfo(accessToken) {
|
|
396
|
+
const client = new RestApiClient({ baseURL: DOCYRUS_API_URL });
|
|
397
|
+
await client.setAccessToken(accessToken);
|
|
398
|
+
try {
|
|
399
|
+
const response = await client.get("/v1/users/me");
|
|
400
|
+
return response.data;
|
|
401
|
+
} catch (error) {
|
|
402
|
+
if (error instanceof AuthenticationError$1) {
|
|
403
|
+
throw new AuthenticationError("Session expired. Please log in again.");
|
|
404
|
+
}
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
async function refreshAccessToken(refreshToken) {
|
|
409
|
+
const tokenUrl = `${DOCYRUS_API_URL}/v1/oauth2/token`;
|
|
410
|
+
try {
|
|
411
|
+
const response = await fetch(tokenUrl, {
|
|
412
|
+
method: "POST",
|
|
413
|
+
headers: {
|
|
414
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
415
|
+
},
|
|
416
|
+
body: new URLSearchParams({
|
|
417
|
+
grant_type: "refresh_token",
|
|
418
|
+
client_id: OAUTH_CLIENT_ID,
|
|
419
|
+
refresh_token: refreshToken
|
|
420
|
+
}).toString()
|
|
421
|
+
});
|
|
422
|
+
if (!response.ok) {
|
|
423
|
+
const errorData = await response.json().catch(() => ({}));
|
|
424
|
+
throw new AuthenticationError(
|
|
425
|
+
errorData.error_description || "Failed to refresh token"
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
const data = await response.json();
|
|
429
|
+
const tokens = {
|
|
430
|
+
accessToken: data.access_token,
|
|
431
|
+
refreshToken: data.refresh_token || refreshToken,
|
|
432
|
+
expiresIn: data.expires_in,
|
|
433
|
+
tokenType: data.token_type
|
|
434
|
+
};
|
|
435
|
+
if (tokens.expiresIn) {
|
|
436
|
+
tokens.expiresAt = Date.now() + tokens.expiresIn * 1e3;
|
|
437
|
+
}
|
|
438
|
+
return tokens;
|
|
439
|
+
} catch (error) {
|
|
440
|
+
if (error instanceof AuthenticationError) {
|
|
441
|
+
throw error;
|
|
442
|
+
}
|
|
443
|
+
if (error instanceof Error && error.message.includes("fetch")) {
|
|
444
|
+
throw new NetworkError();
|
|
445
|
+
}
|
|
446
|
+
throw new AuthenticationError("Session expired. Please log in again.");
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
var SUCCESS_HTML = `
|
|
450
|
+
<!DOCTYPE html>
|
|
451
|
+
<html>
|
|
452
|
+
<head>
|
|
453
|
+
<title>Authentication Successful - Docyrus CLI</title>
|
|
454
|
+
<style>
|
|
455
|
+
* { box-sizing: border-box; }
|
|
456
|
+
body {
|
|
457
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
458
|
+
display: flex;
|
|
459
|
+
justify-content: center;
|
|
460
|
+
align-items: center;
|
|
461
|
+
min-height: 100vh;
|
|
462
|
+
margin: 0;
|
|
463
|
+
background: linear-gradient(to bottom right, #f8fafc, #f1f5f9);
|
|
464
|
+
}
|
|
465
|
+
.container {
|
|
466
|
+
width: 100%;
|
|
467
|
+
max-width: 400px;
|
|
468
|
+
padding: 32px;
|
|
469
|
+
background: white;
|
|
470
|
+
border-radius: 16px;
|
|
471
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.15);
|
|
472
|
+
text-align: center;
|
|
473
|
+
}
|
|
474
|
+
.logo {
|
|
475
|
+
margin-bottom: 24px;
|
|
476
|
+
}
|
|
477
|
+
.logo svg {
|
|
478
|
+
height: 40px;
|
|
479
|
+
width: auto;
|
|
480
|
+
}
|
|
481
|
+
h1 {
|
|
482
|
+
margin: 0 0 8px;
|
|
483
|
+
font-size: 20px;
|
|
484
|
+
font-weight: 600;
|
|
485
|
+
color: #1e293b;
|
|
486
|
+
}
|
|
487
|
+
p {
|
|
488
|
+
margin: 0;
|
|
489
|
+
color: #64748b;
|
|
490
|
+
font-size: 14px;
|
|
491
|
+
}
|
|
492
|
+
</style>
|
|
493
|
+
</head>
|
|
494
|
+
<body>
|
|
495
|
+
<div class="container">
|
|
496
|
+
<div class="logo">
|
|
497
|
+
<svg viewBox="0 0 1325.62 253.55" xmlns="http://www.w3.org/2000/svg">
|
|
498
|
+
<path fill="#dc2626" d="M169.13,133.22l-.07.06v29.48c0,7.81-6.35,14.17-14.16,14.17h-29.48c-2.54,0-4.9-.68-6.96-1.85l-33.13,33.14c2.16,3.21,3.43,7.09,3.43,11.21,0,5.53-2.22,10.54-5.84,14.17-3.62,3.62-8.64,5.84-14.17,5.84s-10.55-2.22-14.17-5.84c-3.62-3.63-5.84-8.64-5.84-14.17,0-11.06,8.96-20.08,20.07-20.08,4.22,0,8.09,1.29,11.3,3.48l33.11-33.11c-1.22-2.07-1.91-4.46-1.91-7.02v-29.48c0-3.24,1.11-6.26,2.97-8.65l-27.31-37.16H19.96V15.5h71.85v65.82l28.44,38.71c1.62-.65,3.39-.98,5.23-.98h29.48c7.81,0,14.17,6.36,14.17,14.17Z"/>
|
|
499
|
+
<path fill="#1c1c1c" d="M241.24,60.57h48.58c12.19,0,23,2.42,32.44,7.25,9.44,4.83,16.77,11.61,21.98,20.34,5.21,8.73,7.82,18.75,7.82,30.05s-2.61,21.32-7.82,30.05c-5.22,8.73-12.54,15.51-21.98,20.34-9.44,4.83-20.25,7.25-32.44,7.25h-48.58V60.57ZM288.83,161.51c9.33,0,17.54-1.81,24.62-5.43,7.08-3.62,12.54-8.7,16.38-15.23,3.84-6.53,5.76-14.08,5.76-22.64s-1.92-16.11-5.76-22.64c-3.84-6.53-9.3-11.61-16.38-15.23-7.08-3.62-15.29-5.43-24.62-5.43h-31.12v86.61h31.12Z"/>
|
|
500
|
+
<path fill="#1c1c1c" d="M430.77,169.5c-9.33-5.1-16.66-12.16-21.98-21.16-5.33-9-7.99-19.04-7.99-30.13s2.66-21.13,7.99-30.13c5.32-9,12.65-16.06,21.98-21.16,9.33-5.1,19.81-7.66,31.45-7.66s21.96,2.55,31.29,7.66c9.33,5.1,16.63,12.13,21.9,21.08,5.27,8.95,7.9,19.02,7.9,30.22s-2.63,21.27-7.9,30.22c-5.27,8.95-12.57,15.97-21.9,21.08-9.33,5.1-19.76,7.66-31.29,7.66s-22.12-2.55-31.45-7.66ZM485.03,156.74c6.75-3.84,12.07-9.14,15.97-15.89,3.9-6.75,5.85-14.3,5.85-22.64s-1.95-15.89-5.85-22.64c-3.9-6.75-9.22-12.05-15.97-15.89-6.75-3.84-14.35-5.76-22.81-5.76s-16.11,1.92-22.97,5.76c-6.86,3.84-12.24,9.14-16.14,15.89-3.9,6.75-5.85,14.3-5.85,22.64s1.95,15.89,5.85,22.64c3.9,6.75,9.28,12.05,16.14,15.89,6.86,3.84,14.52,5.76,22.97,5.76s16.06-1.92,22.81-5.76Z"/>
|
|
501
|
+
<path fill="#1c1c1c" d="M601.78,169.5c-9.28-5.1-16.55-12.13-21.82-21.08-5.27-8.95-7.9-19.02-7.9-30.22s2.63-21.27,7.9-30.22c5.27-8.95,12.57-15.97,21.9-21.08,9.33-5.1,19.76-7.66,31.29-7.66,9,0,17.23,1.51,24.7,4.53,7.46,3.02,13.83,7.49,19.1,13.42l-10.7,10.37c-8.67-9.11-19.49-13.67-32.44-13.67-8.56,0-16.3,1.92-23.22,5.76-6.92,3.84-12.32,9.14-16.22,15.89-3.9,6.75-5.85,14.3-5.85,22.64s1.95,15.89,5.85,22.64c3.9,6.75,9.3,12.05,16.22,15.89,6.92,3.84,14.66,5.76,23.22,5.76,12.84,0,23.66-4.61,32.44-13.83l10.7,10.37c-5.27,5.93-11.67,10.43-19.18,13.5-7.52,3.07-15.78,4.61-24.78,4.61-11.53,0-21.93-2.55-31.2-7.66Z"/>
|
|
502
|
+
<path fill="#1c1c1c" d="M775.59,135.99v39.85h-16.3v-40.18l-45.78-75.09h17.62l36.89,60.76,37.05-60.76h16.3l-45.78,75.42Z"/>
|
|
503
|
+
<path fill="#1c1c1c" d="M946.01,175.84l-24.87-35.4c-3.07.22-5.49.33-7.25.33h-28.49v35.07h-16.47V60.57h44.95c14.93,0,26.68,3.57,35.24,10.7,8.56,7.14,12.84,16.96,12.84,29.48,0,8.89-2.2,16.47-6.59,22.72-4.39,6.26-10.65,10.81-18.77,13.67l27.33,38.7h-17.95ZM937.29,120.02c5.49-4.5,8.23-10.92,8.23-19.27s-2.75-14.74-8.23-19.18c-5.49-4.45-13.45-6.67-23.88-6.67h-27.99v51.87h27.99c10.43,0,18.39-2.25,23.88-6.75Z"/>
|
|
504
|
+
<path fill="#1c1c1c" d="M1033.45,163.98c-8.56-8.78-12.84-21.41-12.84-37.87V60.57h16.47v64.88c0,24.7,10.81,37.05,32.44,37.05,10.54,0,18.61-3.05,24.21-9.14s8.4-15.4,8.4-27.91V60.57h15.97v65.54c0,16.58-4.28,29.23-12.84,37.96-8.56,8.73-20.53,13.09-35.9,13.09s-27.33-4.39-35.9-13.17Z"/>
|
|
505
|
+
<path fill="#1c1c1c" d="M1193.26,173.12c-8.07-2.69-14.41-6.18-19.02-10.46l6.09-12.84c4.39,3.95,10.02,7.16,16.88,9.63,6.86,2.47,13.91,3.7,21.16,3.7,9.55,0,16.69-1.62,21.41-4.86,4.72-3.24,7.08-7.55,7.08-12.93,0-3.95-1.29-7.16-3.87-9.63-2.58-2.47-5.76-4.36-9.55-5.68s-9.14-2.8-16.05-4.45c-8.67-2.08-15.67-4.17-21-6.26-5.33-2.08-9.88-5.29-13.67-9.63-3.79-4.34-5.68-10.18-5.68-17.54,0-6.15,1.62-11.69,4.86-16.63,3.24-4.94,8.15-8.89,14.74-11.86,6.59-2.96,14.76-4.45,24.54-4.45,6.81,0,13.5.88,20.09,2.63,6.59,1.76,12.24,4.28,16.96,7.57l-5.43,13.17c-4.83-3.07-9.99-5.41-15.48-7-5.49-1.59-10.87-2.39-16.14-2.39-9.33,0-16.33,1.7-21,5.1-4.67,3.4-7,7.8-7,13.17,0,3.95,1.32,7.16,3.95,9.63,2.63,2.47,5.9,4.39,9.8,5.76,3.9,1.37,9.19,2.83,15.89,4.36,8.67,2.09,15.64,4.17,20.91,6.26,5.27,2.09,9.8,5.27,13.58,9.55,3.79,4.28,5.68,10.04,5.68,17.29,0,6.04-1.65,11.55-4.94,16.55-3.29,5-8.29,8.95-14.99,11.86-6.7,2.91-14.93,4.36-24.7,4.36-8.67,0-17.04-1.34-25.11-4.03Z"/>
|
|
506
|
+
</svg>
|
|
507
|
+
</div>
|
|
508
|
+
<h1>Authentication Successful</h1>
|
|
509
|
+
<p>You can close this tab and return to the CLI.</p>
|
|
510
|
+
</div>
|
|
511
|
+
</body>
|
|
512
|
+
</html>
|
|
513
|
+
`;
|
|
514
|
+
var ERROR_HTML = (message) => `
|
|
515
|
+
<!DOCTYPE html>
|
|
516
|
+
<html>
|
|
517
|
+
<head>
|
|
518
|
+
<title>Authentication Failed - Docyrus CLI</title>
|
|
519
|
+
<style>
|
|
520
|
+
* { box-sizing: border-box; }
|
|
521
|
+
body {
|
|
522
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
523
|
+
display: flex;
|
|
524
|
+
justify-content: center;
|
|
525
|
+
align-items: center;
|
|
526
|
+
min-height: 100vh;
|
|
527
|
+
margin: 0;
|
|
528
|
+
background: linear-gradient(to bottom right, #f8fafc, #f1f5f9);
|
|
529
|
+
}
|
|
530
|
+
.container {
|
|
531
|
+
width: 100%;
|
|
532
|
+
max-width: 400px;
|
|
533
|
+
padding: 32px;
|
|
534
|
+
background: white;
|
|
535
|
+
border-radius: 16px;
|
|
536
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.15);
|
|
537
|
+
text-align: center;
|
|
538
|
+
}
|
|
539
|
+
.logo {
|
|
540
|
+
margin-bottom: 24px;
|
|
541
|
+
}
|
|
542
|
+
.logo svg {
|
|
543
|
+
height: 40px;
|
|
544
|
+
width: auto;
|
|
545
|
+
}
|
|
546
|
+
h1 {
|
|
547
|
+
margin: 0 0 8px;
|
|
548
|
+
font-size: 20px;
|
|
549
|
+
font-weight: 600;
|
|
550
|
+
color: #1e293b;
|
|
551
|
+
}
|
|
552
|
+
p {
|
|
553
|
+
margin: 0;
|
|
554
|
+
color: #64748b;
|
|
555
|
+
font-size: 14px;
|
|
556
|
+
}
|
|
557
|
+
.error-message {
|
|
558
|
+
margin-top: 16px;
|
|
559
|
+
padding: 12px;
|
|
560
|
+
background: #fef2f2;
|
|
561
|
+
border-radius: 8px;
|
|
562
|
+
color: #dc2626;
|
|
563
|
+
font-size: 13px;
|
|
564
|
+
}
|
|
565
|
+
</style>
|
|
566
|
+
</head>
|
|
567
|
+
<body>
|
|
568
|
+
<div class="container">
|
|
569
|
+
<div class="logo">
|
|
570
|
+
<svg viewBox="0 0 1325.62 253.55" xmlns="http://www.w3.org/2000/svg">
|
|
571
|
+
<path fill="#dc2626" d="M169.13,133.22l-.07.06v29.48c0,7.81-6.35,14.17-14.16,14.17h-29.48c-2.54,0-4.9-.68-6.96-1.85l-33.13,33.14c2.16,3.21,3.43,7.09,3.43,11.21,0,5.53-2.22,10.54-5.84,14.17-3.62,3.62-8.64,5.84-14.17,5.84s-10.55-2.22-14.17-5.84c-3.62-3.63-5.84-8.64-5.84-14.17,0-11.06,8.96-20.08,20.07-20.08,4.22,0,8.09,1.29,11.3,3.48l33.11-33.11c-1.22-2.07-1.91-4.46-1.91-7.02v-29.48c0-3.24,1.11-6.26,2.97-8.65l-27.31-37.16H19.96V15.5h71.85v65.82l28.44,38.71c1.62-.65,3.39-.98,5.23-.98h29.48c7.81,0,14.17,6.36,14.17,14.17Z"/>
|
|
572
|
+
<path fill="#1c1c1c" d="M241.24,60.57h48.58c12.19,0,23,2.42,32.44,7.25,9.44,4.83,16.77,11.61,21.98,20.34,5.21,8.73,7.82,18.75,7.82,30.05s-2.61,21.32-7.82,30.05c-5.22,8.73-12.54,15.51-21.98,20.34-9.44,4.83-20.25,7.25-32.44,7.25h-48.58V60.57ZM288.83,161.51c9.33,0,17.54-1.81,24.62-5.43,7.08-3.62,12.54-8.7,16.38-15.23,3.84-6.53,5.76-14.08,5.76-22.64s-1.92-16.11-5.76-22.64c-3.84-6.53-9.3-11.61-16.38-15.23-7.08-3.62-15.29-5.43-24.62-5.43h-31.12v86.61h31.12Z"/>
|
|
573
|
+
<path fill="#1c1c1c" d="M430.77,169.5c-9.33-5.1-16.66-12.16-21.98-21.16-5.33-9-7.99-19.04-7.99-30.13s2.66-21.13,7.99-30.13c5.32-9,12.65-16.06,21.98-21.16,9.33-5.1,19.81-7.66,31.45-7.66s21.96,2.55,31.29,7.66c9.33,5.1,16.63,12.13,21.9,21.08,5.27,8.95,7.9,19.02,7.9,30.22s-2.63,21.27-7.9,30.22c-5.27,8.95-12.57,15.97-21.9,21.08-9.33,5.1-19.76,7.66-31.29,7.66s-22.12-2.55-31.45-7.66ZM485.03,156.74c6.75-3.84,12.07-9.14,15.97-15.89,3.9-6.75,5.85-14.3,5.85-22.64s-1.95-15.89-5.85-22.64c-3.9-6.75-9.22-12.05-15.97-15.89-6.75-3.84-14.35-5.76-22.81-5.76s-16.11,1.92-22.97,5.76c-6.86,3.84-12.24,9.14-16.14,15.89-3.9,6.75-5.85,14.3-5.85,22.64s1.95,15.89,5.85,22.64c3.9,6.75,9.28,12.05,16.14,15.89,6.86,3.84,14.52,5.76,22.97,5.76s16.06-1.92,22.81-5.76Z"/>
|
|
574
|
+
<path fill="#1c1c1c" d="M601.78,169.5c-9.28-5.1-16.55-12.13-21.82-21.08-5.27-8.95-7.9-19.02-7.9-30.22s2.63-21.27,7.9-30.22c5.27-8.95,12.57-15.97,21.9-21.08,9.33-5.1,19.76-7.66,31.29-7.66,9,0,17.23,1.51,24.7,4.53,7.46,3.02,13.83,7.49,19.1,13.42l-10.7,10.37c-8.67-9.11-19.49-13.67-32.44-13.67-8.56,0-16.3,1.92-23.22,5.76-6.92,3.84-12.32,9.14-16.22,15.89-3.9,6.75-5.85,14.3-5.85,22.64s1.95,15.89,5.85,22.64c3.9,6.75,9.3,12.05,16.22,15.89,6.92,3.84,14.66,5.76,23.22,5.76,12.84,0,23.66-4.61,32.44-13.83l10.7,10.37c-5.27,5.93-11.67,10.43-19.18,13.5-7.52,3.07-15.78,4.61-24.78,4.61-11.53,0-21.93-2.55-31.2-7.66Z"/>
|
|
575
|
+
<path fill="#1c1c1c" d="M775.59,135.99v39.85h-16.3v-40.18l-45.78-75.09h17.62l36.89,60.76,37.05-60.76h16.3l-45.78,75.42Z"/>
|
|
576
|
+
<path fill="#1c1c1c" d="M946.01,175.84l-24.87-35.4c-3.07.22-5.49.33-7.25.33h-28.49v35.07h-16.47V60.57h44.95c14.93,0,26.68,3.57,35.24,10.7,8.56,7.14,12.84,16.96,12.84,29.48,0,8.89-2.2,16.47-6.59,22.72-4.39,6.26-10.65,10.81-18.77,13.67l27.33,38.7h-17.95ZM937.29,120.02c5.49-4.5,8.23-10.92,8.23-19.27s-2.75-14.74-8.23-19.18c-5.49-4.45-13.45-6.67-23.88-6.67h-27.99v51.87h27.99c10.43,0,18.39-2.25,23.88-6.75Z"/>
|
|
577
|
+
<path fill="#1c1c1c" d="M1033.45,163.98c-8.56-8.78-12.84-21.41-12.84-37.87V60.57h16.47v64.88c0,24.7,10.81,37.05,32.44,37.05,10.54,0,18.61-3.05,24.21-9.14s8.4-15.4,8.4-27.91V60.57h15.97v65.54c0,16.58-4.28,29.23-12.84,37.96-8.56,8.73-20.53,13.09-35.9,13.09s-27.33-4.39-35.9-13.17Z"/>
|
|
578
|
+
<path fill="#1c1c1c" d="M1193.26,173.12c-8.07-2.69-14.41-6.18-19.02-10.46l6.09-12.84c4.39,3.95,10.02,7.16,16.88,9.63,6.86,2.47,13.91,3.7,21.16,3.7,9.55,0,16.69-1.62,21.41-4.86,4.72-3.24,7.08-7.55,7.08-12.93,0-3.95-1.29-7.16-3.87-9.63-2.58-2.47-5.76-4.36-9.55-5.68s-9.14-2.8-16.05-4.45c-8.67-2.08-15.67-4.17-21-6.26-5.33-2.08-9.88-5.29-13.67-9.63-3.79-4.34-5.68-10.18-5.68-17.54,0-6.15,1.62-11.69,4.86-16.63,3.24-4.94,8.15-8.89,14.74-11.86,6.59-2.96,14.76-4.45,24.54-4.45,6.81,0,13.5.88,20.09,2.63,6.59,1.76,12.24,4.28,16.96,7.57l-5.43,13.17c-4.83-3.07-9.99-5.41-15.48-7-5.49-1.59-10.87-2.39-16.14-2.39-9.33,0-16.33,1.7-21,5.1-4.67,3.4-7,7.8-7,13.17,0,3.95,1.32,7.16,3.95,9.63,2.63,2.47,5.9,4.39,9.8,5.76,3.9,1.37,9.19,2.83,15.89,4.36,8.67,2.09,15.64,4.17,20.91,6.26,5.27,2.09,9.8,5.27,13.58,9.55,3.79,4.28,5.68,10.04,5.68,17.29,0,6.04-1.65,11.55-4.94,16.55-3.29,5-8.29,8.95-14.99,11.86-6.7,2.91-14.93,4.36-24.7,4.36-8.67,0-17.04-1.34-25.11-4.03Z"/>
|
|
579
|
+
</svg>
|
|
580
|
+
</div>
|
|
581
|
+
<h1>Authentication Failed</h1>
|
|
582
|
+
<p>Something went wrong during authentication.</p>
|
|
583
|
+
<div class="error-message">${message}</div>
|
|
584
|
+
</div>
|
|
585
|
+
</body>
|
|
586
|
+
</html>
|
|
587
|
+
`;
|
|
588
|
+
function generateState() {
|
|
589
|
+
return randomBytes(32).toString("hex");
|
|
590
|
+
}
|
|
591
|
+
function generateCodeVerifier() {
|
|
592
|
+
return randomBytes(32).toString("base64url");
|
|
593
|
+
}
|
|
594
|
+
async function generateCodeChallenge(verifier) {
|
|
595
|
+
const encoder = new TextEncoder();
|
|
596
|
+
const data = encoder.encode(verifier);
|
|
597
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
598
|
+
return Buffer.from(hashBuffer).toString("base64url");
|
|
599
|
+
}
|
|
600
|
+
async function findAvailablePort() {
|
|
601
|
+
return new Promise((resolve, reject) => {
|
|
602
|
+
const tryPort = (port) => {
|
|
603
|
+
if (port > OAUTH_CALLBACK_PORT_MAX) {
|
|
604
|
+
reject(new OAuthError("Could not find an available port for OAuth callback."));
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const server = createServer();
|
|
608
|
+
server.listen(port, "127.0.0.1");
|
|
609
|
+
server.once("listening", () => {
|
|
610
|
+
server.close(() => resolve(port));
|
|
611
|
+
});
|
|
612
|
+
server.once("error", () => {
|
|
613
|
+
tryPort(port + 1);
|
|
614
|
+
});
|
|
615
|
+
};
|
|
616
|
+
tryPort(OAUTH_CALLBACK_PORT_MIN);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
async function startCallbackServer(expectedState) {
|
|
620
|
+
const port = await findAvailablePort();
|
|
621
|
+
let resolveResult;
|
|
622
|
+
let rejectResult;
|
|
623
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
624
|
+
resolveResult = resolve;
|
|
625
|
+
rejectResult = reject;
|
|
626
|
+
});
|
|
627
|
+
const timeoutId = setTimeout(() => {
|
|
628
|
+
rejectResult(new TimeoutError("OAuth authentication timed out."));
|
|
629
|
+
}, OAUTH_TIMEOUT_MS);
|
|
630
|
+
const server = createServer((req, res) => {
|
|
631
|
+
const url = new URL(req.url || "/", `http://127.0.0.1:${port}`);
|
|
632
|
+
if (url.pathname === "/callback") {
|
|
633
|
+
const code = url.searchParams.get("code");
|
|
634
|
+
const state = url.searchParams.get("state");
|
|
635
|
+
const error = url.searchParams.get("error");
|
|
636
|
+
const errorDescription = url.searchParams.get("error_description");
|
|
637
|
+
if (error) {
|
|
638
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
639
|
+
res.end(ERROR_HTML(errorDescription || error));
|
|
640
|
+
rejectResult(new OAuthError(errorDescription || error));
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (!code || !state) {
|
|
644
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
645
|
+
res.end(ERROR_HTML("Missing authorization code or state."));
|
|
646
|
+
rejectResult(new OAuthError("Missing authorization code or state."));
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (state !== expectedState) {
|
|
650
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
651
|
+
res.end(ERROR_HTML("Invalid state parameter. Possible CSRF attack."));
|
|
652
|
+
rejectResult(new OAuthError("Invalid state parameter."));
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
656
|
+
res.end(SUCCESS_HTML);
|
|
657
|
+
resolveResult({ code, state });
|
|
658
|
+
} else {
|
|
659
|
+
res.writeHead(404);
|
|
660
|
+
res.end("Not Found");
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
server.listen(port, "127.0.0.1");
|
|
664
|
+
const cleanup = () => {
|
|
665
|
+
clearTimeout(timeoutId);
|
|
666
|
+
server.close();
|
|
667
|
+
};
|
|
668
|
+
return {
|
|
669
|
+
server,
|
|
670
|
+
port,
|
|
671
|
+
resultPromise,
|
|
672
|
+
cleanup
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
async function openBrowser(url) {
|
|
676
|
+
await open(url);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// src/auth/oauth-auth.ts
|
|
680
|
+
function getOAuthConfig(port) {
|
|
681
|
+
return {
|
|
682
|
+
authorizationUrl: `${DOCYRUS_API_URL}/v1/oauth2/authorize`,
|
|
683
|
+
tokenUrl: `${DOCYRUS_API_URL}/v1/oauth2/token`,
|
|
684
|
+
clientId: OAUTH_CLIENT_ID,
|
|
685
|
+
redirectUri: OAUTH_REDIRECT_URI(port),
|
|
686
|
+
scopes: OAUTH_SCOPES
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
function buildAuthorizationUrl(config, state, codeChallenge) {
|
|
690
|
+
const params = new URLSearchParams({
|
|
691
|
+
response_type: "code",
|
|
692
|
+
client_id: config.clientId,
|
|
693
|
+
redirect_uri: config.redirectUri,
|
|
694
|
+
scope: config.scopes.join(" "),
|
|
695
|
+
state,
|
|
696
|
+
code_challenge: codeChallenge,
|
|
697
|
+
code_challenge_method: "S256"
|
|
698
|
+
});
|
|
699
|
+
return `${config.authorizationUrl}?${params.toString()}`;
|
|
700
|
+
}
|
|
701
|
+
async function exchangeCodeForTokens(config, code, codeVerifier) {
|
|
702
|
+
const response = await fetch(config.tokenUrl, {
|
|
703
|
+
method: "POST",
|
|
704
|
+
headers: {
|
|
705
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
706
|
+
},
|
|
707
|
+
body: new URLSearchParams({
|
|
708
|
+
grant_type: "authorization_code",
|
|
709
|
+
client_id: config.clientId,
|
|
710
|
+
code,
|
|
711
|
+
redirect_uri: config.redirectUri,
|
|
712
|
+
code_verifier: codeVerifier
|
|
713
|
+
}).toString()
|
|
714
|
+
});
|
|
715
|
+
if (!response.ok) {
|
|
716
|
+
const errorText = await response.text();
|
|
717
|
+
throw new OAuthError(`Failed to exchange authorization code: ${errorText}`);
|
|
718
|
+
}
|
|
719
|
+
const data = await response.json();
|
|
720
|
+
const tokens = {
|
|
721
|
+
accessToken: data.access_token,
|
|
722
|
+
refreshToken: data.refresh_token,
|
|
723
|
+
expiresIn: data.expires_in,
|
|
724
|
+
tokenType: data.token_type
|
|
725
|
+
};
|
|
726
|
+
if (tokens.expiresIn) {
|
|
727
|
+
tokens.expiresAt = Date.now() + tokens.expiresIn * 1e3;
|
|
728
|
+
}
|
|
729
|
+
return tokens;
|
|
730
|
+
}
|
|
731
|
+
async function getUserInfoFromToken(accessToken) {
|
|
732
|
+
const client = new RestApiClient({ baseURL: DOCYRUS_API_URL });
|
|
733
|
+
await client.setAccessToken(accessToken);
|
|
734
|
+
try {
|
|
735
|
+
return await client.get("/v1/users/me");
|
|
736
|
+
} catch {
|
|
737
|
+
return void 0;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
async function loginWithOAuth(options = {}) {
|
|
741
|
+
const state = generateState();
|
|
742
|
+
const codeVerifier = generateCodeVerifier();
|
|
743
|
+
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
744
|
+
const { port, resultPromise, cleanup } = await startCallbackServer(state);
|
|
745
|
+
const config = getOAuthConfig(port);
|
|
746
|
+
const authUrl = buildAuthorizationUrl(config, state, codeChallenge);
|
|
747
|
+
try {
|
|
748
|
+
options.onOpeningBrowser?.();
|
|
749
|
+
await openBrowser(authUrl);
|
|
750
|
+
options.onWaitingForAuth?.(authUrl);
|
|
751
|
+
const { code } = await resultPromise;
|
|
752
|
+
const tokens = await exchangeCodeForTokens(config, code, codeVerifier);
|
|
753
|
+
const user = await getUserInfoFromToken(tokens.accessToken);
|
|
754
|
+
return { tokens, user };
|
|
755
|
+
} finally {
|
|
756
|
+
cleanup();
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
var defaults = {
|
|
760
|
+
telemetryEnabled: true
|
|
761
|
+
};
|
|
762
|
+
var ConfigManager = class {
|
|
763
|
+
config;
|
|
764
|
+
constructor() {
|
|
765
|
+
this.config = new Conf({
|
|
766
|
+
projectName: CLI_NAME,
|
|
767
|
+
defaults
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
get(key) {
|
|
771
|
+
return this.config.get(key);
|
|
772
|
+
}
|
|
773
|
+
set(key, value) {
|
|
774
|
+
this.config.set(key, value);
|
|
775
|
+
}
|
|
776
|
+
delete(key) {
|
|
777
|
+
this.config.delete(key);
|
|
778
|
+
}
|
|
779
|
+
getAll() {
|
|
780
|
+
return this.config.store;
|
|
781
|
+
}
|
|
782
|
+
clear() {
|
|
783
|
+
this.config.clear();
|
|
784
|
+
}
|
|
785
|
+
get path() {
|
|
786
|
+
return this.config.path;
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
var configManager = new ConfigManager();
|
|
790
|
+
|
|
791
|
+
export { CLI_NAME, CLI_VERSION, CliTokenManager, DOCYRUS_API_URL, configManager, getTokenManager, getUserInfo, loginWithCredentials, loginWithOAuth, refreshAccessToken };
|
|
792
|
+
//# sourceMappingURL=index.js.map
|
|
793
|
+
//# sourceMappingURL=index.js.map
|