elestio 1.0.3 → 1.2.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/bin/elestio.js CHANGED
@@ -1,5 +1,5 @@
1
- #!/usr/bin/env node
2
-
3
- import { run } from '../src/cli.js';
4
-
5
- run(process.argv.slice(2));
1
+ #!/usr/bin/env node
2
+
3
+ import { run } from '../src/cli.js';
4
+
5
+ run(process.argv.slice(2));
package/package.json CHANGED
@@ -1,42 +1,49 @@
1
- {
2
- "name": "elestio",
3
- "version": "1.0.3",
4
- "description": "Elestio CLI - Deploy and manage services on the Elestio DevOps platform",
5
- "type": "module",
6
- "main": "src/cli.js",
7
- "bin": {
8
- "elestio": "./bin/elestio.js"
9
- },
10
- "scripts": {
11
- "start": "node bin/elestio.js"
12
- },
13
- "keywords": [
14
- "elestio",
15
- "cli",
16
- "devops",
17
- "deploy",
18
- "cloud",
19
- "hosting",
20
- "managed",
21
- "open-source"
22
- ],
23
- "author": "Elestio <support@elest.io>",
24
- "license": "MIT",
25
- "engines": {
26
- "node": ">=18.0.0"
27
- },
28
- "files": [
29
- "bin/",
30
- "src/",
31
- "README.md",
32
- "LICENSE"
33
- ],
34
- "repository": {
35
- "type": "git",
36
- "url": "https://github.com/elestio/elestio-cli"
37
- },
38
- "homepage": "https://elest.io",
39
- "bugs": {
40
- "url": "https://github.com/elestio/elestio-cli/issues"
41
- }
42
- }
1
+ {
2
+ "name": "elestio",
3
+ "version": "1.2.0",
4
+ "description": "Elestio CLI - Deploy and manage services on the Elestio DevOps platform",
5
+ "type": "module",
6
+ "main": "src/cli.js",
7
+ "bin": {
8
+ "elestio": "bin/elestio.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node bin/elestio.js",
12
+ "test": "vitest run",
13
+ "test:watch": "vitest",
14
+ "test:coverage": "vitest run --coverage"
15
+ },
16
+ "keywords": [
17
+ "elestio",
18
+ "cli",
19
+ "devops",
20
+ "deploy",
21
+ "cloud",
22
+ "hosting",
23
+ "managed",
24
+ "open-source"
25
+ ],
26
+ "author": "Elestio <support@elest.io>",
27
+ "license": "MIT",
28
+ "engines": {
29
+ "node": ">=18.0.0"
30
+ },
31
+ "files": [
32
+ "bin/",
33
+ "src/",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/elestio/elestio-cli.git"
40
+ },
41
+ "homepage": "https://elest.io",
42
+ "bugs": {
43
+ "url": "https://github.com/elestio/elestio-cli/issues"
44
+ },
45
+ "devDependencies": {
46
+ "@vitest/coverage-v8": "^2.1.8",
47
+ "vitest": "^2.1.8"
48
+ }
49
+ }
package/src/api.js CHANGED
@@ -1,105 +1,143 @@
1
- import { loadConfig, saveConfig, getCredentials } from './config.js';
2
- import { log } from './utils.js';
3
-
4
- const BASE_URL = 'https://api.elest.io';
5
-
6
- // ── JWT management ──
7
-
8
- function isJwtExpired(config) {
9
- if (!config.jwt || !config.jwtExpiry) return true;
10
- return Date.now() > (config.jwtExpiry - 300000); // 5 min buffer
11
- }
12
-
13
- async function authenticate(email, token) {
14
- const response = await fetch(`${BASE_URL}/api/auth/checkAPIToken`, {
15
- method: 'POST',
16
- headers: { 'Content-Type': 'application/json' },
17
- body: JSON.stringify({ email, token })
18
- });
19
-
20
- const data = await response.json();
21
-
22
- if (data.status !== 'OK' || !data.jwt) {
23
- throw new Error(data.message || 'Authentication failed');
24
- }
25
-
26
- return {
27
- jwt: data.jwt,
28
- jwtExpiry: Date.now() + (23 * 60 * 60 * 1000) // 23h
29
- };
30
- }
31
-
32
- export async function getJwt() {
33
- const config = loadConfig();
34
-
35
- if (!config.email || !config.apiToken) {
36
- throw new Error('Not configured. Run: elestio login');
37
- }
38
-
39
- if (isJwtExpired(config)) {
40
- const auth = await authenticate(config.email, config.apiToken);
41
- config.jwt = auth.jwt;
42
- config.jwtExpiry = auth.jwtExpiry;
43
- saveConfig(config);
44
- }
45
-
46
- return config.jwt;
47
- }
48
-
49
- // ── API requests ──
50
-
51
- export async function apiRequest(endpoint, method = 'POST', body = {}, retried = false) {
52
- const jwt = await getJwt();
53
-
54
- const options = {
55
- method,
56
- headers: { 'Content-Type': 'application/json' }
57
- };
58
-
59
- if (method === 'POST' || method === 'PUT' || method === 'DELETE') {
60
- options.body = JSON.stringify({ jwt, ...body });
61
- }
62
-
63
- const url = method === 'GET' && Object.keys(body).length > 0
64
- ? `${BASE_URL}${endpoint}?${new URLSearchParams({ jwt, ...body })}`
65
- : `${BASE_URL}${endpoint}`;
66
-
67
- const response = await fetch(url, options);
68
-
69
- if (response.status === 401 && !retried) {
70
- const config = loadConfig();
71
- config.jwt = null;
72
- config.jwtExpiry = null;
73
- saveConfig(config);
74
- return apiRequest(endpoint, method, body, true);
75
- }
76
-
77
- const data = await response.json();
78
-
79
- const isAuthError = !retried && (
80
- (data.status === 'error' && data.message?.toLowerCase().includes('auth')) ||
81
- (data.code === 'InvalidToken') ||
82
- (data.message?.toLowerCase().includes('invalid token'))
83
- );
84
-
85
- if (isAuthError) {
86
- const config = loadConfig();
87
- config.jwt = null;
88
- config.jwtExpiry = null;
89
- saveConfig(config);
90
- return apiRequest(endpoint, method, body, true);
91
- }
92
-
93
- return data;
94
- }
95
-
96
- export async function apiRequestNoAuth(endpoint, method = 'GET') {
97
- const url = `${BASE_URL}${endpoint}`;
98
- const response = await fetch(url, {
99
- method,
100
- headers: { 'Content-Type': 'application/json' }
101
- });
102
- return response.json();
103
- }
104
-
105
- export { BASE_URL, authenticate };
1
+ import { loadConfig, saveConfig } from './config.js';
2
+
3
+ const BASE_URL = 'https://api.elest.io';
4
+
5
+ // Requests that exceed this are aborted rather than hanging the CLI forever.
6
+ const REQUEST_TIMEOUT_MS = 60000;
7
+
8
+ // ── JWT management ──
9
+
10
+ function isJwtExpired(config) {
11
+ if (!config.jwt || !config.jwtExpiry) return true;
12
+ return Date.now() > (config.jwtExpiry - 300000); // 5 min buffer
13
+ }
14
+
15
+ /**
16
+ * Wraps fetch with a timeout and turns transport failures into readable errors.
17
+ */
18
+ async function httpRequest(url, options) {
19
+ let response;
20
+
21
+ try {
22
+ response = await fetch(url, { ...options, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
23
+ } catch (err) {
24
+ if (err.name === 'TimeoutError' || err.name === 'AbortError') {
25
+ throw new Error(`Request to ${BASE_URL} timed out after ${REQUEST_TIMEOUT_MS / 1000}s`);
26
+ }
27
+ throw new Error(`Cannot reach ${BASE_URL}: ${err.message}`);
28
+ }
29
+
30
+ return response;
31
+ }
32
+
33
+ /**
34
+ * The API returns HTML on gateway errors, so response.json() alone would surface
35
+ * "Unexpected token <" instead of the real problem.
36
+ */
37
+ async function parseJson(response, endpoint) {
38
+ const text = await response.text();
39
+
40
+ if (text === '') {
41
+ if (response.ok) return {};
42
+ throw new Error(`${endpoint} failed: HTTP ${response.status} ${response.statusText} (empty response)`);
43
+ }
44
+
45
+ try {
46
+ return JSON.parse(text);
47
+ } catch {
48
+ const snippet = text.replace(/\s+/g, ' ').trim().slice(0, 200);
49
+ throw new Error(`${endpoint} returned a non-JSON response (HTTP ${response.status}): ${snippet}`);
50
+ }
51
+ }
52
+
53
+ async function authenticate(email, token) {
54
+ const response = await httpRequest(`${BASE_URL}/api/auth/checkAPIToken`, {
55
+ method: 'POST',
56
+ headers: { 'Content-Type': 'application/json' },
57
+ body: JSON.stringify({ email, token })
58
+ });
59
+
60
+ const data = await parseJson(response, '/api/auth/checkAPIToken');
61
+
62
+ if (data.status !== 'OK' || !data.jwt) {
63
+ throw new Error(data.message || 'Authentication failed');
64
+ }
65
+
66
+ return {
67
+ jwt: data.jwt,
68
+ jwtExpiry: Date.now() + (23 * 60 * 60 * 1000) // 23h
69
+ };
70
+ }
71
+
72
+ export async function getJwt() {
73
+ const config = loadConfig();
74
+
75
+ if (!config.email || !config.apiToken) {
76
+ throw new Error('Not configured. Run: elestio login');
77
+ }
78
+
79
+ if (isJwtExpired(config)) {
80
+ const auth = await authenticate(config.email, config.apiToken);
81
+ saveConfig({ ...config, jwt: auth.jwt, jwtExpiry: auth.jwtExpiry });
82
+ return auth.jwt;
83
+ }
84
+
85
+ return config.jwt;
86
+ }
87
+
88
+ // ── API requests ──
89
+
90
+ function clearJwt() {
91
+ const config = loadConfig();
92
+ saveConfig({ ...config, jwt: null, jwtExpiry: null });
93
+ }
94
+
95
+ export async function apiRequest(endpoint, method = 'POST', body = {}, retried = false) {
96
+ const jwt = await getJwt();
97
+
98
+ const options = {
99
+ method,
100
+ headers: { 'Content-Type': 'application/json' }
101
+ };
102
+
103
+ if (method === 'POST' || method === 'PUT' || method === 'DELETE') {
104
+ options.body = JSON.stringify({ jwt, ...body });
105
+ }
106
+
107
+ // GET routes read parameters from the query string only (backend requirement).
108
+ const url = method === 'GET'
109
+ ? `${BASE_URL}${endpoint}?${new URLSearchParams({ jwt, ...body })}`
110
+ : `${BASE_URL}${endpoint}`;
111
+
112
+ const response = await httpRequest(url, options);
113
+
114
+ if (response.status === 401 && !retried) {
115
+ clearJwt();
116
+ return apiRequest(endpoint, method, body, true);
117
+ }
118
+
119
+ const data = await parseJson(response, endpoint);
120
+
121
+ const isAuthError = !retried && (
122
+ (data.status === 'error' && data.message?.toLowerCase().includes('auth')) ||
123
+ (data.code === 'InvalidToken') ||
124
+ (data.message?.toLowerCase().includes('invalid token'))
125
+ );
126
+
127
+ if (isAuthError) {
128
+ clearJwt();
129
+ return apiRequest(endpoint, method, body, true);
130
+ }
131
+
132
+ return data;
133
+ }
134
+
135
+ export async function apiRequestNoAuth(endpoint, method = 'GET') {
136
+ const response = await httpRequest(`${BASE_URL}${endpoint}`, {
137
+ method,
138
+ headers: { 'Content-Type': 'application/json' }
139
+ });
140
+ return parseJson(response, endpoint);
141
+ }
142
+
143
+ export { BASE_URL, authenticate };