@youkno/edge-cli 1.20.2314
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/.pnp.cjs +8793 -0
- package/.pnp.loader.mjs +2126 -0
- package/.yarn/install-state.gz +0 -0
- package/README.md +44 -0
- package/dist/commands/config.js +25 -0
- package/dist/commands/healthCheck.js +30 -0
- package/dist/commands/login.js +55 -0
- package/dist/commands/product.js +24 -0
- package/dist/commands/shell.js +180 -0
- package/dist/commands/token.js +23 -0
- package/dist/commands/uploadUsers.js +54 -0
- package/dist/default-edge-api-config.json +61 -0
- package/dist/index.js +40 -0
- package/dist/lib/auth.js +451 -0
- package/dist/lib/config.js +216 -0
- package/dist/lib/http.js +15 -0
- package/dist/lib/request.js +24 -0
- package/dist/lib/types.js +2 -0
- package/package.json +27 -0
- package/src/commands/config.ts +28 -0
- package/src/commands/healthCheck.ts +35 -0
- package/src/commands/login.ts +59 -0
- package/src/commands/product.ts +24 -0
- package/src/commands/shell.ts +213 -0
- package/src/commands/token.ts +26 -0
- package/src/commands/uploadUsers.ts +63 -0
- package/src/default-edge-api-config.json +61 -0
- package/src/index.ts +45 -0
- package/src/lib/auth.ts +527 -0
- package/src/lib/config.ts +253 -0
- package/src/lib/http.ts +12 -0
- package/src/lib/request.ts +23 -0
- package/src/lib/types.ts +35 -0
- package/tsconfig.json +14 -0
package/dist/lib/auth.js
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.listLogins = listLogins;
|
|
7
|
+
exports.setDefaultLogin = setDefaultLogin;
|
|
8
|
+
exports.logout = logout;
|
|
9
|
+
exports.login = login;
|
|
10
|
+
exports.resolveAccessToken = resolveAccessToken;
|
|
11
|
+
exports.resolveAuthHeader = resolveAuthHeader;
|
|
12
|
+
exports.getAuthHeaderFromOverride = getAuthHeaderFromOverride;
|
|
13
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
14
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
15
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
16
|
+
const node_child_process_1 = require("node:child_process");
|
|
17
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
18
|
+
const AUTH_DIR = process.env.EDGE_CLI_AUTH_DIR
|
|
19
|
+
? node_path_1.default.resolve(process.env.EDGE_CLI_AUTH_DIR)
|
|
20
|
+
: node_path_1.default.join(node_os_1.default.homedir(), ".edge-cli-auth");
|
|
21
|
+
const AUTH_FILE = node_path_1.default.join(AUTH_DIR, "accounts.json");
|
|
22
|
+
const DEFAULT_DEVICE_NAME = "edge-cli";
|
|
23
|
+
function scopeKey(cfg) {
|
|
24
|
+
return `${cfg.product}/${cfg.env}`;
|
|
25
|
+
}
|
|
26
|
+
function ensureAuthDir() {
|
|
27
|
+
if (!node_fs_1.default.existsSync(AUTH_DIR)) {
|
|
28
|
+
node_fs_1.default.mkdirSync(AUTH_DIR, { recursive: true, mode: 0o700 });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function readDb() {
|
|
32
|
+
ensureAuthDir();
|
|
33
|
+
if (!node_fs_1.default.existsSync(AUTH_FILE)) {
|
|
34
|
+
return { scopes: {} };
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const raw = node_fs_1.default.readFileSync(AUTH_FILE, "utf8");
|
|
38
|
+
const parsed = JSON.parse(raw);
|
|
39
|
+
if (!parsed.scopes || typeof parsed.scopes !== "object") {
|
|
40
|
+
return { scopes: {} };
|
|
41
|
+
}
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return { scopes: {} };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function writeDb(db) {
|
|
49
|
+
ensureAuthDir();
|
|
50
|
+
node_fs_1.default.writeFileSync(AUTH_FILE, `${JSON.stringify(db, null, 2)}\n`, { mode: 0o600 });
|
|
51
|
+
}
|
|
52
|
+
function getScope(db, cfg) {
|
|
53
|
+
const key = scopeKey(cfg);
|
|
54
|
+
if (!db.scopes[key]) {
|
|
55
|
+
db.scopes[key] = { default: "", accounts: {} };
|
|
56
|
+
}
|
|
57
|
+
if (!db.scopes[key].accounts) {
|
|
58
|
+
db.scopes[key].accounts = {};
|
|
59
|
+
}
|
|
60
|
+
return db.scopes[key];
|
|
61
|
+
}
|
|
62
|
+
function nowEpoch() {
|
|
63
|
+
return Math.floor(Date.now() / 1000);
|
|
64
|
+
}
|
|
65
|
+
function listLogins(cfg) {
|
|
66
|
+
const db = readDb();
|
|
67
|
+
const scope = getScope(db, cfg);
|
|
68
|
+
return Object.keys(scope.accounts)
|
|
69
|
+
.sort((a, b) => a.localeCompare(b))
|
|
70
|
+
.map((email) => ({ email, isDefault: scope.default === email }));
|
|
71
|
+
}
|
|
72
|
+
function setDefaultLogin(cfg, email) {
|
|
73
|
+
const db = readDb();
|
|
74
|
+
const scope = getScope(db, cfg);
|
|
75
|
+
if (!scope.accounts[email]) {
|
|
76
|
+
throw new Error(`Account not found for ${cfg.product}/${cfg.env}: ${email}`);
|
|
77
|
+
}
|
|
78
|
+
scope.default = email;
|
|
79
|
+
writeDb(db);
|
|
80
|
+
}
|
|
81
|
+
function authStateHeader(cfg) {
|
|
82
|
+
return `${cfg.product}.${cfg.env}`;
|
|
83
|
+
}
|
|
84
|
+
function authDeviceId() {
|
|
85
|
+
return `edge-cli:${node_os_1.default.hostname() || "unknown-host"}`;
|
|
86
|
+
}
|
|
87
|
+
function decodeJwtPayload(token) {
|
|
88
|
+
const parts = token.split(".");
|
|
89
|
+
if (parts.length < 2) {
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
93
|
+
const pad = b64.length % 4 === 0 ? "" : "=".repeat(4 - (b64.length % 4));
|
|
94
|
+
const decoded = Buffer.from(b64 + pad, "base64").toString("utf8");
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(decoded);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function extractEmailFromToken(accessToken) {
|
|
103
|
+
const payload = decodeJwtPayload(accessToken);
|
|
104
|
+
const email = payload.email;
|
|
105
|
+
if (typeof email === "string" && email.trim()) {
|
|
106
|
+
return email.trim();
|
|
107
|
+
}
|
|
108
|
+
return "unknown@local";
|
|
109
|
+
}
|
|
110
|
+
function getAuthHeaderFromOverride(auth) {
|
|
111
|
+
if (!auth) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
return `Basic ${Buffer.from(auth).toString("base64")}`;
|
|
115
|
+
}
|
|
116
|
+
async function requestTokenEndpoint(cfg, endpoint, payload) {
|
|
117
|
+
const url = `${cfg.baseUrl.replace(/\/$/, "")}/api/v1/auth/${endpoint}`;
|
|
118
|
+
const response = await fetch(url, {
|
|
119
|
+
method: "POST",
|
|
120
|
+
headers: {
|
|
121
|
+
"Content-Type": "application/json",
|
|
122
|
+
"X-edge-state": authStateHeader(cfg)
|
|
123
|
+
},
|
|
124
|
+
body: JSON.stringify(payload)
|
|
125
|
+
});
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
const body = await response.text();
|
|
128
|
+
throw new Error(`Auth ${endpoint} failed (${response.status}): ${body}`);
|
|
129
|
+
}
|
|
130
|
+
if (endpoint === "logout") {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
return (await response.json());
|
|
134
|
+
}
|
|
135
|
+
async function authExchangeFirebaseToken(cfg, firebaseToken) {
|
|
136
|
+
const resp = await requestTokenEndpoint(cfg, "exchange", {
|
|
137
|
+
firebaseToken,
|
|
138
|
+
deviceId: authDeviceId(),
|
|
139
|
+
deviceName: DEFAULT_DEVICE_NAME
|
|
140
|
+
});
|
|
141
|
+
if (!resp?.accessToken) {
|
|
142
|
+
throw new Error("Token exchange did not return accessToken");
|
|
143
|
+
}
|
|
144
|
+
return resp;
|
|
145
|
+
}
|
|
146
|
+
async function authRefreshToken(cfg, refreshToken) {
|
|
147
|
+
const resp = await requestTokenEndpoint(cfg, "refresh", {
|
|
148
|
+
refreshToken,
|
|
149
|
+
deviceId: authDeviceId(),
|
|
150
|
+
deviceName: DEFAULT_DEVICE_NAME
|
|
151
|
+
});
|
|
152
|
+
if (!resp?.accessToken) {
|
|
153
|
+
throw new Error("Token refresh did not return accessToken");
|
|
154
|
+
}
|
|
155
|
+
return resp;
|
|
156
|
+
}
|
|
157
|
+
async function authLogoutRefreshToken(cfg, refreshToken) {
|
|
158
|
+
await requestTokenEndpoint(cfg, "logout", { refreshToken });
|
|
159
|
+
}
|
|
160
|
+
async function logout(cfg, email) {
|
|
161
|
+
const db = readDb();
|
|
162
|
+
const scope = getScope(db, cfg);
|
|
163
|
+
const target = email ?? scope.default;
|
|
164
|
+
if (!target) {
|
|
165
|
+
throw new Error(`No account to logout for ${cfg.product}/${cfg.env}`);
|
|
166
|
+
}
|
|
167
|
+
const account = scope.accounts[target];
|
|
168
|
+
if (!account) {
|
|
169
|
+
throw new Error(`Account not found for ${cfg.product}/${cfg.env}: ${target}`);
|
|
170
|
+
}
|
|
171
|
+
if (account.refreshToken) {
|
|
172
|
+
try {
|
|
173
|
+
await authLogoutRefreshToken(cfg, account.refreshToken);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// best effort
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
delete scope.accounts[target];
|
|
180
|
+
if (scope.default === target) {
|
|
181
|
+
const left = Object.keys(scope.accounts).sort((a, b) => a.localeCompare(b));
|
|
182
|
+
scope.default = left[0] ?? "";
|
|
183
|
+
}
|
|
184
|
+
writeDb(db);
|
|
185
|
+
return target;
|
|
186
|
+
}
|
|
187
|
+
function storeTokens(cfg, email, tokenResp) {
|
|
188
|
+
const db = readDb();
|
|
189
|
+
const scope = getScope(db, cfg);
|
|
190
|
+
const now = nowEpoch();
|
|
191
|
+
scope.accounts[email] = {
|
|
192
|
+
accessToken: tokenResp.accessToken,
|
|
193
|
+
refreshToken: tokenResp.refreshToken,
|
|
194
|
+
accessExpEpoch: now + (tokenResp.accessTokenExpiresInSec ?? 0),
|
|
195
|
+
refreshExpEpoch: tokenResp.refreshToken ? now + (tokenResp.refreshTokenExpiresInSec ?? 0) : 0
|
|
196
|
+
};
|
|
197
|
+
scope.default = email;
|
|
198
|
+
writeDb(db);
|
|
199
|
+
}
|
|
200
|
+
function parseCallbackQuery(queryString) {
|
|
201
|
+
const query = queryString.startsWith("?") ? queryString.slice(1) : queryString;
|
|
202
|
+
const params = new URLSearchParams(query);
|
|
203
|
+
const read = (...keys) => {
|
|
204
|
+
for (const k of keys) {
|
|
205
|
+
const value = params.get(k);
|
|
206
|
+
if (value) {
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return undefined;
|
|
211
|
+
};
|
|
212
|
+
return {
|
|
213
|
+
email: read("email", "user", "username", "userEmail"),
|
|
214
|
+
accessToken: read("accessToken", "access_token", "jwt", "token"),
|
|
215
|
+
refreshToken: read("refreshToken", "refresh_token"),
|
|
216
|
+
firebaseToken: read("firebaseToken", "firebase_token", "idToken", "id_token"),
|
|
217
|
+
code: read("code", "authCode")
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function openBrowser(url) {
|
|
221
|
+
const platform = process.platform;
|
|
222
|
+
let command;
|
|
223
|
+
let args;
|
|
224
|
+
if (platform === "darwin") {
|
|
225
|
+
command = "open";
|
|
226
|
+
args = [url];
|
|
227
|
+
}
|
|
228
|
+
else if (platform === "win32") {
|
|
229
|
+
command = "cmd";
|
|
230
|
+
args = ["/c", "start", "", url];
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
command = "xdg-open";
|
|
234
|
+
args = [url];
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
const child = (0, node_child_process_1.spawn)(command, args, { stdio: "ignore", detached: true });
|
|
238
|
+
child.unref();
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function startAuthListener(timeoutSec) {
|
|
246
|
+
const tokenKeys = new Set([
|
|
247
|
+
"accessToken",
|
|
248
|
+
"access_token",
|
|
249
|
+
"jwt",
|
|
250
|
+
"token",
|
|
251
|
+
"refreshToken",
|
|
252
|
+
"refresh_token",
|
|
253
|
+
"firebaseToken",
|
|
254
|
+
"firebase_token",
|
|
255
|
+
"idToken",
|
|
256
|
+
"id_token",
|
|
257
|
+
"code",
|
|
258
|
+
"authCode"
|
|
259
|
+
]);
|
|
260
|
+
return new Promise((resolve, reject) => {
|
|
261
|
+
let callbackResolved = false;
|
|
262
|
+
let callbackSettled = false;
|
|
263
|
+
let startupSettled = false;
|
|
264
|
+
let callbackResolve = () => { };
|
|
265
|
+
let callbackReject = () => { };
|
|
266
|
+
const callback = new Promise((res, rej) => {
|
|
267
|
+
callbackResolve = res;
|
|
268
|
+
callbackReject = rej;
|
|
269
|
+
});
|
|
270
|
+
const server = node_http_1.default.createServer((req, res) => {
|
|
271
|
+
const reqUrl = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
272
|
+
const hasToken = Array.from(reqUrl.searchParams.keys()).some((k) => tokenKeys.has(k));
|
|
273
|
+
if (hasToken) {
|
|
274
|
+
callbackResolved = true;
|
|
275
|
+
clearTimeout(timeout);
|
|
276
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
277
|
+
res.end("<html><body><h3>Authentication successful.</h3>You can close this tab.</body></html>");
|
|
278
|
+
const query = reqUrl.search;
|
|
279
|
+
server.close(() => {
|
|
280
|
+
if (!callbackSettled) {
|
|
281
|
+
callbackSettled = true;
|
|
282
|
+
callbackResolve(query);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
288
|
+
res.end(`<html><body><h3>Completing authentication...</h3>
|
|
289
|
+
<script>
|
|
290
|
+
const hash = window.location.hash ? window.location.hash.substring(1) : "";
|
|
291
|
+
if (hash && !window.location.search) {
|
|
292
|
+
window.location.replace(window.location.pathname + "?" + hash);
|
|
293
|
+
}
|
|
294
|
+
</script>
|
|
295
|
+
You can close this tab.</body></html>`);
|
|
296
|
+
});
|
|
297
|
+
const timeout = setTimeout(() => {
|
|
298
|
+
if (!callbackResolved) {
|
|
299
|
+
server.close(() => {
|
|
300
|
+
if (!callbackSettled) {
|
|
301
|
+
callbackSettled = true;
|
|
302
|
+
callbackReject(new Error("Authentication timed out or callback not received."));
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}, timeoutSec * 1000);
|
|
307
|
+
let redirectUrl = "";
|
|
308
|
+
server.listen(0, "127.0.0.1", () => {
|
|
309
|
+
const address = server.address();
|
|
310
|
+
if (!address || typeof address === "string") {
|
|
311
|
+
clearTimeout(timeout);
|
|
312
|
+
server.close(() => {
|
|
313
|
+
if (!startupSettled) {
|
|
314
|
+
startupSettled = true;
|
|
315
|
+
reject(new Error("Unable to acquire callback listener port."));
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
redirectUrl = `http://127.0.0.1:${address.port}/callback`;
|
|
321
|
+
if (!startupSettled) {
|
|
322
|
+
startupSettled = true;
|
|
323
|
+
resolve({ redirectUrl, callback });
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
async function login(cfg) {
|
|
329
|
+
if (!cfg.consoleBaseUrl) {
|
|
330
|
+
throw new Error(`No console URL configured for ${cfg.product}/${cfg.env}.`);
|
|
331
|
+
}
|
|
332
|
+
const listener = await startAuthListener(180);
|
|
333
|
+
const redirectUrl = listener.redirectUrl;
|
|
334
|
+
const loginUrl = new URL(`${cfg.consoleBaseUrl.replace(/\/$/, "")}/admin/sign-in`);
|
|
335
|
+
loginUrl.searchParams.set("redirect_url", redirectUrl);
|
|
336
|
+
loginUrl.searchParams.set("redirectUrl", redirectUrl);
|
|
337
|
+
loginUrl.searchParams.set("redirect_uri", redirectUrl);
|
|
338
|
+
process.stdout.write("Opening browser for authentication...\n");
|
|
339
|
+
process.stdout.write(`Login URL: ${loginUrl.toString()}\n`);
|
|
340
|
+
if (!openBrowser(loginUrl.toString())) {
|
|
341
|
+
process.stdout.write("Open this URL in your browser to continue:\n");
|
|
342
|
+
process.stdout.write(`${loginUrl.toString()}\n`);
|
|
343
|
+
}
|
|
344
|
+
const query = await listener.callback;
|
|
345
|
+
const parsed = parseCallbackQuery(query);
|
|
346
|
+
if (parsed.code) {
|
|
347
|
+
throw new Error("Authentication callback returned code-based response, exchange not implemented.");
|
|
348
|
+
}
|
|
349
|
+
let tokenResp;
|
|
350
|
+
if (parsed.firebaseToken) {
|
|
351
|
+
tokenResp = await authExchangeFirebaseToken(cfg, parsed.firebaseToken);
|
|
352
|
+
}
|
|
353
|
+
else if (parsed.accessToken) {
|
|
354
|
+
tokenResp = {
|
|
355
|
+
accessToken: parsed.accessToken,
|
|
356
|
+
refreshToken: parsed.refreshToken,
|
|
357
|
+
accessTokenExpiresInSec: 3600,
|
|
358
|
+
refreshTokenExpiresInSec: parsed.refreshToken ? 2_592_000 : 0
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
if (!tokenResp?.accessToken) {
|
|
362
|
+
throw new Error("Authentication callback did not include usable token data.");
|
|
363
|
+
}
|
|
364
|
+
const email = parsed.email ?? extractEmailFromToken(tokenResp.accessToken);
|
|
365
|
+
storeTokens(cfg, email, tokenResp);
|
|
366
|
+
return email;
|
|
367
|
+
}
|
|
368
|
+
async function validateAccessToken(cfg, accessToken) {
|
|
369
|
+
const url = new URL(`${cfg.baseUrl.replace(/\/$/, "")}/shell`);
|
|
370
|
+
url.searchParams.set("cmd", "help");
|
|
371
|
+
const response = await fetch(url, {
|
|
372
|
+
method: "GET",
|
|
373
|
+
headers: {
|
|
374
|
+
"X-edge-state": authStateHeader(cfg),
|
|
375
|
+
Authorization: `Bearer ${accessToken}`
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
return response.status === 200 || response.status === 204;
|
|
379
|
+
}
|
|
380
|
+
async function refreshDefaultToken(cfg) {
|
|
381
|
+
const db = readDb();
|
|
382
|
+
const scope = getScope(db, cfg);
|
|
383
|
+
const email = scope.default;
|
|
384
|
+
if (!email) {
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
const account = scope.accounts[email];
|
|
388
|
+
if (!account?.refreshToken) {
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
const refreshed = await authRefreshToken(cfg, account.refreshToken);
|
|
392
|
+
const merged = {
|
|
393
|
+
accessToken: refreshed.accessToken,
|
|
394
|
+
refreshToken: refreshed.refreshToken || account.refreshToken,
|
|
395
|
+
accessTokenExpiresInSec: refreshed.accessTokenExpiresInSec,
|
|
396
|
+
refreshTokenExpiresInSec: refreshed.refreshTokenExpiresInSec
|
|
397
|
+
};
|
|
398
|
+
storeTokens(cfg, email, merged);
|
|
399
|
+
return merged.accessToken;
|
|
400
|
+
}
|
|
401
|
+
async function resolveAccessToken(cfg, forceRefresh = false) {
|
|
402
|
+
const override = getAuthHeaderFromOverride(cfg.auth);
|
|
403
|
+
if (override) {
|
|
404
|
+
throw new Error("Cannot derive JWT token when --auth basic credentials are used.");
|
|
405
|
+
}
|
|
406
|
+
const db = readDb();
|
|
407
|
+
const scope = getScope(db, cfg);
|
|
408
|
+
const email = scope.default;
|
|
409
|
+
if (!email) {
|
|
410
|
+
throw new Error(`No JWT session for ${cfg.product}/${cfg.env}. Run 'edge-cli login'.`);
|
|
411
|
+
}
|
|
412
|
+
const account = scope.accounts[email];
|
|
413
|
+
if (!account?.accessToken) {
|
|
414
|
+
throw new Error(`No access token for ${email}. Run 'edge-cli login'.`);
|
|
415
|
+
}
|
|
416
|
+
let accessToken = account.accessToken;
|
|
417
|
+
const expiry = account.accessExpEpoch ?? 0;
|
|
418
|
+
const now = nowEpoch();
|
|
419
|
+
if (forceRefresh && account.refreshToken) {
|
|
420
|
+
const refreshed = await refreshDefaultToken(cfg);
|
|
421
|
+
if (refreshed) {
|
|
422
|
+
return refreshed;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (expiry > 0 && expiry <= now + 60 && account.refreshToken) {
|
|
426
|
+
try {
|
|
427
|
+
const refreshed = await refreshDefaultToken(cfg);
|
|
428
|
+
if (refreshed) {
|
|
429
|
+
accessToken = refreshed;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
// continue with existing token
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (!(await validateAccessToken(cfg, accessToken)) && account.refreshToken) {
|
|
437
|
+
const refreshed = await refreshDefaultToken(cfg);
|
|
438
|
+
if (refreshed) {
|
|
439
|
+
accessToken = refreshed;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return accessToken;
|
|
443
|
+
}
|
|
444
|
+
async function resolveAuthHeader(cfg) {
|
|
445
|
+
const override = getAuthHeaderFromOverride(cfg.auth);
|
|
446
|
+
if (override) {
|
|
447
|
+
return override;
|
|
448
|
+
}
|
|
449
|
+
const accessToken = await resolveAccessToken(cfg);
|
|
450
|
+
return `Bearer ${accessToken}`;
|
|
451
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveEffectiveConfig = resolveEffectiveConfig;
|
|
7
|
+
exports.setDefaultProduct = setDefaultProduct;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const promises_1 = __importDefault(require("node:readline/promises"));
|
|
12
|
+
const default_edge_api_config_json_1 = __importDefault(require("../default-edge-api-config.json"));
|
|
13
|
+
const EDGE_API_CONFIG_URL = "https://cdn.youkno.ai/tools/edge-api-config.json";
|
|
14
|
+
const DEFAULT_EDGE_API_PATH = node_path_1.default.join(node_os_1.default.homedir(), ".edge-api");
|
|
15
|
+
const FILE_KEYS = ["PRODUCT", "ENV", "BASE_URL", "CLIENT_ID", "CLIENT_AUTH"];
|
|
16
|
+
function expandPath(inputPath) {
|
|
17
|
+
if (inputPath.startsWith("~/")) {
|
|
18
|
+
return node_path_1.default.join(node_os_1.default.homedir(), inputPath.slice(2));
|
|
19
|
+
}
|
|
20
|
+
if (inputPath === "~") {
|
|
21
|
+
return node_os_1.default.homedir();
|
|
22
|
+
}
|
|
23
|
+
return inputPath;
|
|
24
|
+
}
|
|
25
|
+
function parseEdgeApiFile(filePath) {
|
|
26
|
+
const out = {};
|
|
27
|
+
if (!node_fs_1.default.existsSync(filePath)) {
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
const content = node_fs_1.default.readFileSync(filePath, "utf8");
|
|
31
|
+
const lines = content.split(/\r?\n/);
|
|
32
|
+
for (const lineRaw of lines) {
|
|
33
|
+
const line = lineRaw.trim();
|
|
34
|
+
if (!line || line.startsWith("#")) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const eqIndex = line.indexOf("=");
|
|
38
|
+
if (eqIndex <= 0) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const key = line.slice(0, eqIndex).trim();
|
|
42
|
+
if (!FILE_KEYS.includes(key)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
let value = line.slice(eqIndex + 1).trim();
|
|
46
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
47
|
+
value = value.slice(1, -1);
|
|
48
|
+
}
|
|
49
|
+
out[key] = value;
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
function toCliEnv(env) {
|
|
54
|
+
if (env === "devlocal" || env === "devel" || env === "prod") {
|
|
55
|
+
return env;
|
|
56
|
+
}
|
|
57
|
+
return "prod";
|
|
58
|
+
}
|
|
59
|
+
function resolveProductsConfig() {
|
|
60
|
+
const candidatePaths = [
|
|
61
|
+
process.env.EDGE_API_CONFIG_PATH ? node_path_1.default.resolve(expandPath(process.env.EDGE_API_CONFIG_PATH)) : undefined,
|
|
62
|
+
node_path_1.default.resolve(process.cwd(), "edge-api-config.json"),
|
|
63
|
+
node_path_1.default.resolve(process.cwd(), "..", "edge-api-config.json"),
|
|
64
|
+
node_path_1.default.resolve(__dirname, "..", "..", "..", "edge-api-config.json")
|
|
65
|
+
].filter((p) => Boolean(p));
|
|
66
|
+
for (const candidatePath of candidatePaths) {
|
|
67
|
+
if (!node_fs_1.default.existsSync(candidatePath)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const content = node_fs_1.default.readFileSync(candidatePath, "utf8");
|
|
72
|
+
return JSON.parse(content);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Try next candidate
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return default_edge_api_config_json_1.default;
|
|
79
|
+
}
|
|
80
|
+
function findProduct(product, config) {
|
|
81
|
+
const products = config.api?.products ?? {};
|
|
82
|
+
if (products[product]) {
|
|
83
|
+
return products[product];
|
|
84
|
+
}
|
|
85
|
+
for (const productConfig of Object.values(products)) {
|
|
86
|
+
const aliases = productConfig.aliases ?? [];
|
|
87
|
+
if (aliases.includes(product)) {
|
|
88
|
+
return productConfig;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
function listProducts(config) {
|
|
94
|
+
const products = config.api?.products ?? {};
|
|
95
|
+
return Object.entries(products)
|
|
96
|
+
.filter(([, value]) => value.active !== false)
|
|
97
|
+
.map(([key]) => key)
|
|
98
|
+
.sort((a, b) => a.localeCompare(b));
|
|
99
|
+
}
|
|
100
|
+
async function selectProductInteractively(products) {
|
|
101
|
+
const rl = promises_1.default.createInterface({
|
|
102
|
+
input: process.stdin,
|
|
103
|
+
output: process.stdout
|
|
104
|
+
});
|
|
105
|
+
try {
|
|
106
|
+
process.stdout.write("Select product:\n");
|
|
107
|
+
products.forEach((p, idx) => {
|
|
108
|
+
process.stdout.write(` ${idx + 1}) ${p}\n`);
|
|
109
|
+
});
|
|
110
|
+
while (true) {
|
|
111
|
+
const inputRaw = await rl.question("Choose product number or name: ");
|
|
112
|
+
const input = inputRaw.trim().toLowerCase();
|
|
113
|
+
if (!input) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (/^\d+$/.test(input)) {
|
|
117
|
+
const index = Number(input) - 1;
|
|
118
|
+
if (index >= 0 && index < products.length) {
|
|
119
|
+
return products[index];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (products.includes(input)) {
|
|
123
|
+
return input;
|
|
124
|
+
}
|
|
125
|
+
process.stdout.write("Invalid selection.\n");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
rl.close();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function resolveProductBaseUrl(product, config) {
|
|
133
|
+
const resolved = findProduct(product, config);
|
|
134
|
+
return resolved?.baseUrl ?? config.api?.defaultBaseUrl;
|
|
135
|
+
}
|
|
136
|
+
function resolveConsoleBaseUrl(product, env, config) {
|
|
137
|
+
const resolved = findProduct(product, config);
|
|
138
|
+
if (!resolved) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
return env === "prod" ? resolved.consoleBaseProd : resolved.consoleBaseTest;
|
|
142
|
+
}
|
|
143
|
+
function readMergedFileConfig(options) {
|
|
144
|
+
const configFiles = [
|
|
145
|
+
"/etc/edge-api",
|
|
146
|
+
node_path_1.default.resolve(process.cwd(), ".edge-api"),
|
|
147
|
+
node_path_1.default.join(node_os_1.default.homedir(), ".edge-api")
|
|
148
|
+
];
|
|
149
|
+
if (options.configPath) {
|
|
150
|
+
configFiles.unshift(node_path_1.default.resolve(process.cwd(), options.configPath));
|
|
151
|
+
}
|
|
152
|
+
const fileConfig = configFiles
|
|
153
|
+
.filter((filePath, index, arr) => arr.indexOf(filePath) === index)
|
|
154
|
+
.map((filePath) => ({ filePath, cfg: parseEdgeApiFile(filePath) }))
|
|
155
|
+
.filter((entry) => Object.keys(entry.cfg).length > 0);
|
|
156
|
+
const merged = {};
|
|
157
|
+
for (const entry of fileConfig) {
|
|
158
|
+
Object.assign(merged, entry.cfg);
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
merged,
|
|
162
|
+
usedFiles: fileConfig.map((entry) => entry.filePath)
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
async function resolveEffectiveConfig(options) {
|
|
166
|
+
const { merged, usedFiles } = readMergedFileConfig(options);
|
|
167
|
+
const env = toCliEnv(options.env ?? merged.ENV);
|
|
168
|
+
const productsConfig = resolveProductsConfig();
|
|
169
|
+
let product = (options.product ?? merged.PRODUCT ?? "").trim().toLowerCase();
|
|
170
|
+
if (!product) {
|
|
171
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
172
|
+
const available = listProducts(productsConfig);
|
|
173
|
+
if (available.length > 0) {
|
|
174
|
+
product = await selectProductInteractively(available);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!product) {
|
|
179
|
+
throw new Error("No product specified. Pass --product <name>, set PRODUCT in ~/.edge-api, or run 'edge-cli product:default <name>'.");
|
|
180
|
+
}
|
|
181
|
+
let baseUrl = options.baseUrl ?? merged.BASE_URL;
|
|
182
|
+
if (!baseUrl) {
|
|
183
|
+
if (env === "devlocal") {
|
|
184
|
+
baseUrl = "http://localhost:8080";
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
baseUrl = resolveProductBaseUrl(product, productsConfig);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (!baseUrl) {
|
|
191
|
+
throw new Error(`Unable to resolve base URL. Set --base-url, BASE_URL in .edge-api, or configure product '${product}' in edge-api-config.json (${EDGE_API_CONFIG_URL}).`);
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
product,
|
|
195
|
+
env,
|
|
196
|
+
baseUrl,
|
|
197
|
+
consoleBaseUrl: resolveConsoleBaseUrl(product, env, productsConfig),
|
|
198
|
+
clientId: options.clientId ?? merged.CLIENT_ID,
|
|
199
|
+
auth: options.auth ?? merged.CLIENT_AUTH,
|
|
200
|
+
configFiles: usedFiles
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function setDefaultProduct(product, filePath) {
|
|
204
|
+
const targetFile = node_path_1.default.resolve(expandPath(filePath ?? DEFAULT_EDGE_API_PATH));
|
|
205
|
+
const targetDir = node_path_1.default.dirname(targetFile);
|
|
206
|
+
node_fs_1.default.mkdirSync(targetDir, { recursive: true });
|
|
207
|
+
const existing = node_fs_1.default.existsSync(targetFile) ? node_fs_1.default.readFileSync(targetFile, "utf8") : "";
|
|
208
|
+
const lines = existing ? existing.split(/\r?\n/) : [];
|
|
209
|
+
const kept = lines.filter((line) => !line.trim().startsWith("PRODUCT="));
|
|
210
|
+
if (product && product.trim()) {
|
|
211
|
+
kept.push(`PRODUCT=${product.trim().toLowerCase()}`);
|
|
212
|
+
}
|
|
213
|
+
const content = kept.filter((line, idx, arr) => !(idx === arr.length - 1 && line === "")).join("\n");
|
|
214
|
+
node_fs_1.default.writeFileSync(targetFile, content ? `${content}\n` : "", "utf8");
|
|
215
|
+
return targetFile;
|
|
216
|
+
}
|
package/dist/lib/http.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.basicAuthHeader = basicAuthHeader;
|
|
4
|
+
exports.getText = getText;
|
|
5
|
+
function basicAuthHeader(username, password) {
|
|
6
|
+
return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
|
|
7
|
+
}
|
|
8
|
+
async function getText(url, headers) {
|
|
9
|
+
const response = await fetch(url, {
|
|
10
|
+
method: "GET",
|
|
11
|
+
headers
|
|
12
|
+
});
|
|
13
|
+
const body = await response.text();
|
|
14
|
+
return { status: response.status, body };
|
|
15
|
+
}
|