@openluxeco/cli 0.5.0 → 0.5.2
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/bin/openluxe.js +6 -4
- package/package.json +1 -1
- package/src/api.js +34 -6
- package/src/auth.js +6 -1
- package/src/cli.js +1 -1
- package/src/config.js +8 -4
package/bin/openluxe.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { run } from '../src/cli.js';
|
|
4
|
+
import { load } from '../src/config.js';
|
|
5
|
+
|
|
3
6
|
// Opt-in escape hatch for self-signed certs (local Herd / staging boxes).
|
|
4
|
-
// Default stays fully secure — only
|
|
5
|
-
|
|
7
|
+
// Default stays fully secure — relaxes only when explicitly requested, or when
|
|
8
|
+
// this device was logged in with the flag (remembered per saved base URL).
|
|
9
|
+
if (process.env.OPENLUXE_INSECURE === '1' || process.argv.includes('--insecure') || load().insecure) {
|
|
6
10
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
7
11
|
}
|
|
8
12
|
|
|
9
|
-
import { run } from '../src/cli.js';
|
|
10
|
-
|
|
11
13
|
run(process.argv.slice(2)).catch((e) => {
|
|
12
14
|
console.error(`\x1b[31m✗ ${e?.message || e}\x1b[0m`);
|
|
13
15
|
process.exit(1);
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -8,6 +8,29 @@ export class ApiError extends Error {
|
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Node's fetch buries the real failure (TLS, DNS, refused) in `error.cause` and
|
|
13
|
+
* reports only "fetch failed" — surface the cause plus an actionable hint.
|
|
14
|
+
*/
|
|
15
|
+
function networkErrorMessage(e, url) {
|
|
16
|
+
const cause = e.cause || {};
|
|
17
|
+
const code = cause.code || '';
|
|
18
|
+
const causeMsg = cause.message || '';
|
|
19
|
+
let origin = '';
|
|
20
|
+
try { origin = ' (' + new URL(url).origin + ')'; } catch { /* keep plain */ }
|
|
21
|
+
|
|
22
|
+
let msg = `Network error: ${causeMsg || e.message}${origin}`;
|
|
23
|
+
if (/CERT|certificate|SSL|TLS/i.test(code + ' ' + causeMsg)) {
|
|
24
|
+
msg += '\n Untrusted/self-signed certificate — for a local dev server, retry with OPENLUXE_INSECURE=1'
|
|
25
|
+
+ '\n (logging in with it set remembers the choice for that server)';
|
|
26
|
+
} else if (code === 'ENOTFOUND' || code === 'EAI_AGAIN') {
|
|
27
|
+
msg += '\n Host not found — check the API base (OPENLUXE_API_URL, or `openluxe auth status` for the stored base)';
|
|
28
|
+
} else if (code === 'ECONNREFUSED') {
|
|
29
|
+
msg += '\n Connection refused — is the server running at that base URL?';
|
|
30
|
+
}
|
|
31
|
+
return msg;
|
|
32
|
+
}
|
|
33
|
+
|
|
11
34
|
/**
|
|
12
35
|
* Perform an authenticated v1 API request. `path` is everything after
|
|
13
36
|
* /api/v1 (e.g. "/contacts"). `query` is an object, `body` is JSON-able.
|
|
@@ -36,7 +59,7 @@ export async function request(method, path, { query, body, token, base } = {}) {
|
|
|
36
59
|
try {
|
|
37
60
|
res = await fetch(url, init);
|
|
38
61
|
} catch (e) {
|
|
39
|
-
throw new ApiError(0,
|
|
62
|
+
throw new ApiError(0, networkErrorMessage(e, url));
|
|
40
63
|
}
|
|
41
64
|
|
|
42
65
|
const text = await res.text();
|
|
@@ -55,11 +78,16 @@ export async function request(method, path, { query, body, token, base } = {}) {
|
|
|
55
78
|
/** Unauthenticated POST used only by the device-auth handshake. */
|
|
56
79
|
export async function postPublic(base, path, body) {
|
|
57
80
|
const url = base.replace(/\/$/, '') + '/api/v1' + path;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
81
|
+
let res;
|
|
82
|
+
try {
|
|
83
|
+
res = await fetch(url, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
86
|
+
body: JSON.stringify(body || {}),
|
|
87
|
+
});
|
|
88
|
+
} catch (e) {
|
|
89
|
+
throw new ApiError(0, networkErrorMessage(e, url));
|
|
90
|
+
}
|
|
63
91
|
const text = await res.text();
|
|
64
92
|
let parsed;
|
|
65
93
|
try { parsed = text ? JSON.parse(text) : null; } catch { parsed = text; }
|
package/src/auth.js
CHANGED
|
@@ -50,10 +50,15 @@ export async function login({ base } = {}) {
|
|
|
50
50
|
const poll = await postPublic(apiBase, '/cli/auth/poll', { device_code });
|
|
51
51
|
|
|
52
52
|
if (poll.ok && poll.data?.status === 'authorized') {
|
|
53
|
-
|
|
53
|
+
// Logging in with TLS verification relaxed (OPENLUXE_INSECURE=1 /
|
|
54
|
+
// --insecure) remembers that choice for this base, so follow-up
|
|
55
|
+
// commands against the same dev server don't need the flag.
|
|
56
|
+
const insecure = process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0';
|
|
57
|
+
save({ base: apiBase, token: poll.data.token, user: poll.data.user, insecure });
|
|
54
58
|
const who = poll.data.user?.email || poll.data.user?.name || 'your account';
|
|
55
59
|
console.log(`\x1b[32m✓ Signed in as ${who}\x1b[0m`);
|
|
56
60
|
console.log(` Token stored at ${credentialsPath}`);
|
|
61
|
+
if (insecure) console.log(' TLS verification stays relaxed for this server (remembered from login).');
|
|
57
62
|
console.log('');
|
|
58
63
|
console.log(' Your API use is governed by the OpenLuxe API & CLI Terms');
|
|
59
64
|
console.log(` and all referenced policies — see \x1b[36m${apiBase}/api-terms\x1b[0m`);
|
package/src/cli.js
CHANGED
|
@@ -187,7 +187,7 @@ async function callApi(method, path, { positionals = [], flags = {}, body }) {
|
|
|
187
187
|
printInsufficientCredits(e.body || {});
|
|
188
188
|
process.exit(1);
|
|
189
189
|
}
|
|
190
|
-
console.error(C.red(
|
|
190
|
+
console.error(C.red('✗ ' + (e.status ? e.status + ' ' : '') + e.message));
|
|
191
191
|
if (e.body && typeof e.body === 'object') console.error(C.dim(JSON.stringify(e.body, null, 2)));
|
|
192
192
|
if (e.status === 401) console.error(C.dim(' Run: openluxe auth login'));
|
|
193
193
|
process.exit(1);
|
package/src/config.js
CHANGED
|
@@ -14,7 +14,7 @@ const DEFAULT_BASE = process.env.OPENLUXE_API_URL || 'https://openluxe.co';
|
|
|
14
14
|
|
|
15
15
|
export function load() {
|
|
16
16
|
if (!existsSync(FILE)) {
|
|
17
|
-
return { base: DEFAULT_BASE, token: null, user: null };
|
|
17
|
+
return { base: DEFAULT_BASE, token: null, user: null, insecure: false };
|
|
18
18
|
}
|
|
19
19
|
try {
|
|
20
20
|
const data = JSON.parse(readFileSync(FILE, 'utf8'));
|
|
@@ -22,15 +22,19 @@ export function load() {
|
|
|
22
22
|
base: process.env.OPENLUXE_API_URL || data.base || DEFAULT_BASE,
|
|
23
23
|
token: data.token || null,
|
|
24
24
|
user: data.user || null,
|
|
25
|
+
// The remembered insecure opt-in is scoped to the base it was saved
|
|
26
|
+
// for — pointing OPENLUXE_API_URL elsewhere must stay fully secure.
|
|
27
|
+
insecure: data.insecure === true
|
|
28
|
+
&& (!process.env.OPENLUXE_API_URL || process.env.OPENLUXE_API_URL === data.base),
|
|
25
29
|
};
|
|
26
30
|
} catch {
|
|
27
|
-
return { base: DEFAULT_BASE, token: null, user: null };
|
|
31
|
+
return { base: DEFAULT_BASE, token: null, user: null, insecure: false };
|
|
28
32
|
}
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
export function save({ base, token, user }) {
|
|
35
|
+
export function save({ base, token, user, insecure }) {
|
|
32
36
|
mkdirSync(DIR, { recursive: true });
|
|
33
|
-
writeFileSync(FILE, JSON.stringify({ base, token, user }, null, 2));
|
|
37
|
+
writeFileSync(FILE, JSON.stringify({ base, token, user, ...(insecure ? { insecure: true } : {}) }, null, 2));
|
|
34
38
|
// Credentials file holds a bearer token — lock it down to the owner.
|
|
35
39
|
try { chmodSync(FILE, 0o600); } catch { /* best effort (e.g. Windows) */ }
|
|
36
40
|
}
|